【JavaScript】Object とは似て非なる Array の異常さを明らかにする ~exotic object とは~

# イントロダクション

そもそも array とは0番目の値、1番目の値、...と一列に値を並べたもので、 object は key-value のペアの集まりであり、まったく異なるデータ構造である。

しかし、array は object としての性質も持っており、自由にプロパティの追加などができる。

```js
const arr = [1, 2, 3]
arr.prop = "value"
```
一方で、`length` プロパティに代入できる値に制限があるなど、object とは異なる部分もある。

```js
const a = []
a.length = -2  // Uncaught RangeError: Invalid array length
```

[^prop]: プロパティには2種類あり、data property と accessor property である。前者は普通のプロパティで、一つの値を格納するだけである。後者は getter と setter と呼ばれる関数によってそれぞれ"値の取得"、"値の代入"を実現するものである。

「単に setter/getter 持ちのプロパティ(accessor property[^prop])なのでは？」と思うかもしれないが、実はそうではなく、普通の data property[^prop] である。この記事では、このような array の異常性を列挙した上で、それを実現する仕組みを説明する。

# 1. 持っているメソッドが違う

array object は `.slice()` メソッドなど、通常のオブジェクトにはないメソッドを持っているが、これは簡単に説明がつく。

array object は `Array.prototype` を継承しており、さらにこれは `Object.prototype` を継承している。`Array.prototype` がいくつかのメソッドを定義しているため、array object は object が持つメソッドだけでなく array 特有のメソッドにもアクセスできるのである。array が iterable であるのも、`Array.prototype` が `[Symbol.iterator]` メソッドを持っているからである。

また、以下のように `toString()` メソッドの挙動が異なることも、「`Object.prototype` が持つ `toString()` メソッドを `Array.prototype` がオーバーライドしているから」と説明がつく。

```js
const obj = {0: 1, 1: 2}
const arr = [1, 2]

obj.toString()  // "[object Object]"  <- Object.prototype.toString()
arr.toString()  // "1,2"              <- Array.prototype.toString()
```

![prototype chain.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/446383/e98b9a74-14ef-2461-fc8b-9f961fc53976.png)

# 2. `length` プロパティ

array object の `length` プロパティは特別なものであり、普通のプロパティとは異なる振る舞いをする。ここで、`length` が data property[^prop] であることを確認しておこう。

```js
Object.getOwnPropertyDescriptor([], "length")
// {value: 0, writable: true, enumerable: false, configurable: false}
```

もし data property ではなく accessor property であれば、`value` と `writable` の代わりに `get` と `set` が存在するはずである。**`length` プロパティには getter も setter もない**ことが分かる。

# 3. `length` 以上のインデックスに代入すると `length` も増える

`length` 以上のインデックスに対して代入をすると、`length` の値はそのインデックス+1になる。

```js
const arr = []
arr.length  // 0

arr[2] = 10
arr.length  // 3
arr         // [empty, empty, 10]
```

当たり前と言えばそうだが、object として考えると異常な振る舞いである。**新しいプロパティを追加しただけで、別の data property の値が変わるというのは、普通の object ではありえない**。

ちなみに、要素を `delete` しても `length` は変わらない。

```js
const arr = [1, 2, 3]
arr.length  // 3
delete arr[2]
arr.length  // 3
arr         // [1, 2, empty]
```

# 4. `length` プロパティを non-writable にすると `length` 以上のインデックスに代入できない

`length` プロパティの `writable` 属性を `false` にして[^len]値を変更できなくすると、`length` 以上のインデックスに対する代入ができなくなる。

[^len]: array の `length` プロパティは最初から non-configurable なので `writable` 属性を変更できないはずでは？と思うかもしれないが、実は一般的に non-configurable であっても `writable` を `true` から `false` にすることは特別に許されている。仕様の該当部分: [9.1.6.3 ValidateAndApplyPropertyDescriptor](https://www.ecma-international.org/ecma-262/10.0/index.html#sec-validateandapplypropertydescriptor)

```js
const arr = Object.defineProperty([], "length", {writable: false})
arr[3] = 10
arr         // []
arr.length  // 0
```

strict モードならエラーが発生する。

```js
"use strict"
const arr = Object.defineProperty([], "length", {writable: false})
arr[3] = 10  // Uncaught TypeError
```

3\. のことを考えれば納得できるが、やはり普通の object ではありえない挙動である。

# 5. `length` プロパティを変えると実際の要素数も変わる

`length` を減らすと、`length` 以上のインデックスを持つ要素は削除される。

```js
const arr = [1, 2, 3, 4, 5]
arr.length = 3
arr  // [1, 2, 3]
```


該当するプロパティも消えていることが分かる。

```js
Object.getOwnPropertyNames(arr)  // [ '0', '1', '2', 'length' ]
```

このように、`length` プロパティを変えることで他のプロパティに影響を与えるという点においても、array object が異常であることが伺えるだろう。

逆に `length` を増やすと、empty な要素が末尾に追加される。

```js
const arr = [1, 2, 3]
arr.length = 6
arr  // [1, 2, 3, empty, empty, empty]
```

しかし、プロパティの数が増えるわけではない。`length` は 6 であるのに、`3` `4` `5`のプロパティは存在しないことが分かる。

```js
Object.getOwnPropertyNames(arr)  // [ '0', '1', '2', 'length' ]
```

empty な要素というのは、**該当するプロパティが存在しない要素**を意味するのである。


# 6. `length` を減らすときに non-configurable な要素があるとそこで止まる

`length` を減らそうとすると、削除される予定の要素の中にもし `configurable` 属性が `false` であるものがいくつかあれば、その中で最も大きいインデックスの所で `length` が止まる。これは、non-configurable なプロパティは削除できないからであるが、それでも中途半端に長さが減るというのは意外である。

```js
const arr = ["a", "b", "c", "d", "e"]

// プロパティ "2" を non-configurable にする
Object.defineProperty(arr, "2", {
  configurable: false
})

arr.length = 0
arr.length  // 3
arr         // ["a", "b", "c"]
```

![array length change.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/446383/c750b062-d76a-0459-40ba-495de24761e6.png)



これも、strict モードではエラーが発生するが、`length` が減らなくなるわけではない。

```js
"use strict"
const arr = ["a", "b", "c", "d", "e"]

Object.defineProperty(arr, "2", {
  configurable: false
})

arr.length = 0  // Uncaught TypeError: Cannot delete property '2' of [object Array]
arr.length      // 3
```

# 7. `length` は 0 から 2^32 - 1 まで

array object の 長さは 0 から 2^32 - 1 までの整数値しかとれない。これも array の長さを表すという点では不思議なことではないが、setter があるわけでもないのにある種の validation が行われるのはやはり異常である。

```js
const arr = []
arr.length = -1       // Uncaught RangeError: Invalid array length
arr.length = 1.5      // Uncaught RangeError: Invalid array length
arr.length = 2 ** 32  // Uncaught RangeError: Invalid array length
```

ちなみに、上記の条件さえみたせば数値の string 表現でもよい。

```js
const arr = []
arr.length = "3"
arr.length  // 3
```

もっと言えば、`Number()` 関数に渡して有効な数値になる値ならばなんでもよい。

```js
const arr = []
const length = { valueOf: () => 3 }
Number(length)  // 3
arr.length = length
arr.length  // 3
```

ちなみに内部的にはこの数値変換はなぜか2回行われる。

```js
const arr = []
const length = { valueOf: () => { console.log("oops"); return 3 } }
arr.length = length  // "oops" が2回出力される
```

# 8. インデックスは 0 から 2^32 - 2 まで

`length` と同様に、 array インデックスとして有効な整数は `0` から `2^32 - 2` までであり、それ以外の値をキーにもつプロパティを作っても、配列の要素とはならない。

```js
const arr = []

arr[2 ** 32 - 1] = 1  // 範囲外
arr.length  // 0
Object.getOwnPropertyNames(arr)  // ["length", "4294967295"] (プロパティとしては追加されている)

arr[2 ** 32 - 2] = 1  // 範囲内
arr.length  // 4294967295
```

# タネ明かし

ここまでで述べたような array object の特殊な挙動はいったいどのようにして実現されているのだろうか？

実は JavaScript の object は「プログラマーからは一切見えない内部的なメソッド」を持っている。これらのメソッドはその object に対して何らかの操作をしたときにそれに対応したものが呼び出されるようになっている。Array object はこのうち `[[DefineOwnProperty]]` というメソッドだけ、通常の object とは異なった定義がされているのである。下記のリンクは ECMAScript 2019 の仕様の該当部分である。

- [通常 object の `[[DefineOwnProperty]]`](https://www.ecma-international.org/ecma-262/10.0/index.html#sec-ordinary-object-internal-methods-and-internal-slots-defineownproperty-p-desc)
- [array object の `[[DefineOwnProperty]]`](https://www.ecma-international.org/ecma-262/10.0/index.html#sec-array-exotic-objects-defineownproperty-p-desc) (後で軽く説明する)

内部メソッドは object が作られるときに、その object の内部スロットに明示的にセットされる。例えば array が作られるときは下のように、通常 object とは異なる `[[DefineOwnProperty]]` メソッドがセットされるのである。

> ...
5\. Let A be a newly created Array exotic object.
6\. Set A's essential internal methods except for [[DefineOwnProperty]] to the default ordinary object definitions specified in 9.1.
7\. **Set A.[[DefineOwnProperty]] as specified in 9.4.2.1.**
...
(from [9.4.2.2 ArrayCreate](https://www.ecma-international.org/ecma-262/10.0/index.html#sec-arraycreate))

Side Note: 普通のプロパティやメソッドと異なり、内部メソッドは**継承されない**。例えば array の `slice` メソッドは `Array.prototype` から継承したものであり、array が作られるたびにわざわざメソッドをセットしているわけではない。しかし、内部メソッドは **object を作るたびに毎回その内部スロットにセットするようになっている**。

このように、通常の object の内部メソッドをオーバーライドしているような object を **exotic object** といい(exotic=風変わりな)、例としては Array、TypedArray、`arguments`、`bind` した function、proxy object などがある。

この `[[DefineOwnProperty]]` メソッドは下の図のように、プロパティへの代入が行われるときに呼び出される(あくまで仕様を追うときに参考になればいいという図)。

![js property assign under the hood](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/446383/fc666503-2568-0de1-f200-9acadacedffc.png)

下のように、array object は `[[DefineOwnProperty]]` メソッドにおいてプロパティ名が `length` のときとインデックスであるときに特別な処理をしていることが分かる。ここまでで説明した array object の特殊な挙動は、この内部メソッドを見ればすべて説明がつく。

![array_defineownproperty.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/446383/f97cd2d2-8a28-011f-fddf-654f4e76a617.png)

# まとめ

array object は基本的には object であるが、普段使う JavaScript の知識だけでは説明できないような振る舞いをする。このような奇妙な振る舞いを理解するためには、言語仕様で定義されている内部メソッドなどの低レベルな所まで見なければいけない。筆者は array の「object っぽいけど違う」所が気持ち悪く感じ、ネット上で調べても答えが得られなかったため、自分で言語仕様を読んで理解し記事を書くに至ったが、もし他の誰かの参考になれば幸いである。

# 仕様の該当箇所

本文中にあげた部分は除く。

- 代入式: [12.15.4 Runtime Semantics: Evaluation](https://www.ecma-international.org/ecma-262/10.0/index.html#sec-assignment-operators-runtime-semantics-evaluation)
- exotic object: [4.3.7 exotic object](https://www.ecma-international.org/ecma-262/10.0/index.html#sec-exotic-object)
- array index の定義: [6.1.7 The Object Type](https://www.ecma-international.org/ecma-262/10.0/index.html#array-index)
- 数値への変換処理: [7.1.3 ToNumber ( argument )](https://www.ecma-international.org/ecma-262/10.0/index.html#sec-tonumber)