【追記あり】ES2022 Array#at がちょっとおかしい #fix_ecmascript_at

<details><summary>【2021/9/13 追記】</summary><div>

既に Stage 4 になっているので諦めていたんですが、流石に見逃せないかなと思ったので TC39 の Discourse にトピックをたててみました。意見がある方はこちらにお願いします。

https://es.discourse.group/t/fix-at/983

議論に伴って私が実際に欲しかったものをモジュールにして公開してみました。

https://github.com/petamoriken/safe-at

それといまいちユーザーからの声が伝わっていない感じがしたのでハッシュタグ [#fix_ecmascript_at](https://twitter.com/hashtag/fix_ecmascript_at) を用意してみました。協力をよろしくおねがいします。
</div></details>

<details><summary>【2021/9/13 追記2】</summary><div>

`String#char{At, CodeAt}` という存在を忘れてたんですが、この似た名前のメソッドたちが引数を整数に丸めるのに `String#at` が丸めないのはたしかに変だということに気づいてしまったので、自分からはこれ以上はこの件については口をつぐむことにしました[^necessary]。

[^necessary]: 理屈は納得したんですが、果たしてこの `at` は追加する必要があったのか……とは思います。個人的に規約などでこのメソッドの使用は禁止します。

プロパティアクセスに対したシンタックスシュガーが欲しければ、何か別の提案としてあげる必要がありそうですね……。

</div></details>

【2021/9/16 追記】

`at` メソッドを一律で禁止するよりかは数値リテラルは許容するといいのではという指摘を受けました。確かにその通りだと思います。

https://twitter.com/yuta0801_/status/1438132509661745158

折角なので eslint のルールとして書いてみました。ご自由にお使いください。

```json
{
  "no-restricted-syntax": [
    "error",
    {
      "selector": "CallExpression[callee.property.name='at']:not([arguments.0.type='Literal'],[arguments.0.type='UnaryExpression'][arguments.0.argument.type='Literal'])",
      "message": "at method accepts only a literal argument"
    }
  ]
}
```

【追記ここまで】

---

https://twitter.com/petamoriken/status/1435170216304988160

これについて解説します。ちなみに答えは `1` です。

## 配列のプロパティアクセスについて

JavaScript においてオブジェクトのプロパティにアクセスするキーは文字列（とシンボル）だけとなっています。その他の値をキーに入れた場合、文字列に暗黙的型変換されます[^impl]。ちなみに配列はオブジェクトの一種です。

[^impl]: 仕様の上ではそうですが、実装では同じ結果さえ返せばいいのでどうなっているかわかりません。普通配列に対して数値でアクセスすることが多いため、実装ではそれを前提とした最適化を行っているかもしれません。無駄に文字列で配列のプロパティにアクセスするコードを書くのは辞めたほうがいいです。

```js
const arr = [1, 2, 3];
console.log(arr["0"]); // => 1
console.log(arr["1"]); // => 2
console.log(arr["10"]); // => undefined

// 以下キーが "0" に変換されて 1 を返す
console.log(arr[0]);
console.log(arr[-0]);
console.log(arr[0n]);
console.log(arr[{ toString() { return "0"; } }]);

// プロパティが存在しないため undefined を返す
console.log(arr["foo"]);
console.log(arr[NaN]); // arr["NaN"]
console.log(arr[1.5]); // arr["1.5"]
```

https://262.ecma-international.org/12.0/#sec-topropertykey

配列の要素を後ろからアクセスしたい場合は `Array#length` を使います。

```js
const arr = [1, 2, 3];
console.log(arr[arr.length - 1]); // => 3
console.log(arr[arr.length - 2]); // => 2
```

## ES2022 `Array#at` について

配列を後ろからアクセスするのにわざわざ `Array#length` を使わないといけないのが煩わしいと長い間言われてきました。そこで導入されたのが ES2022 `Array#at` です[^others]。引数に負の数を入れると後ろからアクセスできます。

[^others]: `TypedArray` や `String` に対しても同様に `at` メソッドが定義されています。中身はほとんど同じです。

```js
const arr = [1, 2, 3];

console.log(arr.at(0)); // => 1
console.log(arr.at(1)); // => 2
console.log(arr.at(10)); // => undefined

console.log(arr.at(-1)); // => 3
console.log(arr.at(-2)); // => 2
```

これでわざわざ長ったらしく記述する必要がなくなりました。

### 奇妙な動作

ところで `Array#at` では通常のプロパティアクセスとは異なり、以下のような奇妙な事が起きます。

```js
const arr = [1, 2, 3];

console.log(arr.at("foo")); // => 1
console.log(arr.at(NaN)); // => 1
console.log(arr.at(1.5)); // => 2
```

どうしてこのような事が起きるのでしょうか。

通常のプロパティアクセスでは文字列に変換されるところですが、`Array#at` では負の数の対応をしないといけないためその前に引数を数値に変換する必要があります。仕様を見てみましょう。

```
Array.prototype.at ( index )

  1. Let O be ? ToObject(this value).
  2. Let len be ? LengthOfArrayLike(O).
  3. Let relativeIndex be ? ToIntegerOrInfinity(index).
  4. If relativeIndex ≥ 0, then
        a. Let k be relativeIndex.
  5. Else,
        a. Let k be len + relativeIndex.
  6. If k < 0 or k ≥ len, return undefined.
  7. Return ? Get(O, ! ToString(𝔽(k))).
```

https://tc39.es/ecma262/#sec-array.prototype.at

ここで引数の `index` はその型によらずに必ず `ToIntegerOrInfinity` を通ることがわかります。これを使って数値に変換しているようです。その仕様が以下の通りです。

```
ToIntegerOrInfinity ( argument )

The abstract operation ToIntegerOrInfinity takes argument argument.
It converts argument to an integer, +∞, or -∞. It performs the following steps when called:

  1. Let number be ? ToNumber(argument).
  2. If number is NaN, +0𝔽, or -0𝔽, return 0.
  3. If number is +∞𝔽, return +∞.
  4. If number is -∞𝔽, return -∞.
  5. Let integer be floor(abs(ℝ(number))).
  6. If number < +0𝔽, set integer to -integer.
  7. Return integer.
```

https://tc39.es/ecma262/#sec-tointegerorinfinity

`ToNumber` で数値に変換し[^bigint]、**`NaN` は `0` にして返し、小数点以下のある数値ではそれを取り除いて整数値にして返す**という処理を行うことがわかります。これが奇妙な動作の原因です。

[^bigint]: `ToNumber` に `bigint` の値を入れると `TypeError` を投げます。よって `Array#at` に `bigint` は入れられません。

……いやいやいや。単に `ToNumber` のみを使って変換し、その後で整数値以外をはじくような処理を行えばこのような変なことにならない気がします。どうしてこのような仕様になってしまったんでしょう。

実はこれに関連して数値にならない文字列（`"foo"` など）や `NaN` を入れた場合について指摘した issue があります。

https://github.com/tc39/proposal-relative-indexing-method/issues/49

TC39 メンバーの ljharb さんの返答は以下の通り。

> I see the argument that `.at(NaN)` should perhaps always return `undefined` - however, given that the proposal is stage 3 and shipping in multiple browsers, it's unlikely we'd be able to make such a change.

……というわけで誰も指摘がされなかったままブラウザに実装されてしまったのでもう手遅れというのがオチみたいです。

【2021/9/13 追記】

どうやらこれは `Array#slice` などのインデックスを受け取るメソッドで既に広く使われているやり方なので、`Array#at` もそれに合わせたということみたいです。

どう考えても使い勝手が悪そうなんですが、何故そのような決定になったのでしょう。既に Stage 4 になっている仕様に対してこうやって意見を言うのもおかしい話ではあるんですが……。

【追記ここまで】

## 結び

`Array#at` は引数が整数値かどうかチェックしてくれません。その上勝手に整数値に変換します。何らかの計算結果を `Array#at` に入れる場合には注意しましょう[^length]。

[^length]: 正直なところ、こんな罠を警戒するくらいなら多少記述が長くなっても `Array#length` を使ったほうがマシだと個人的に思います。

ところで現在 Stage 2 Change Array by Copy という提案があります。

https://github.com/tc39/proposal-change-array-by-copy

この提案には `Array#withAt` というものが含まれており、指定したプロパティの値のみを変更した新しい配列を作ることが出来ます。

```js
const arr1 = [1, 2, 3];
const arr2 = arr1.withAt(1, 4); // メソッド名についてはまだ議論中です
console.log(arr1); // => Array(3) [1, 2, 3]
console.log(arr2); // => Array(3) [1, 4, 3]
```

現状このメソッドでは `Array#at` の反省を活かしてか、キーに相当する引数が整数値でない場合は `RangeError` を投げる仕様になっています。

https://tc39.es/proposal-change-array-by-copy/#sec-array.prototype.withAt

JavaScript の言語仕様には一貫性がないことがよくあります。ES5 からある `Array#indexOf` は `NaN` を検知できませんが ES2015 `Array#includes` では検知できるなどは結構有名なのではないでしょうか。

```js
const arr = [1, 2, 3, NaN];
console.log(arr.indexOf(NaN)); // => -1
console.log(arr.includes(NaN)); // => true
```

こういった話は JavaScript の面白いところでもあり嫌われるところでもあるのかなと思います。ECMAScript の提案を追うのは割と楽しいので皆さんもよかったらどうぞ。