array-like object っていったい何？iterable との違いは？言語仕様に立ち返って説明する

# イントロ

JavaScript では array-like object (配列みたいなオブジェクト) という用語が使われるが、MDN を探してもハッキリとした定義は見当たらない。この記事では ECMAScript の仕様に基づいて、array-like object がどういうものなのかを解説する。

必要な知識: プロパティ、配列、オブジェクト、プロトタイプ、メソッド

# よく知られている array-like object の性質

例えば `document.getElementsByClassName()` の返り値は [`HTMLCollection`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCollection) であり、これは array-like である。`HTMLCollection` は `.slice()` などの配列メソッドを持っていないが、array-like であるため以下のように強制的に配列メソッドを呼び出すことができる。

```js
// HTMLCollection
const btns = document.getElementsByClassName("btn")

// array
const btnsArray = Array.prototype.slice.call(btns)
```

`.call(btns)` で `this` を `btns` にすることで、強制的に配列の `.slice()` メソッドを `btns` に対して呼び出している。仕組みを詳しく知りたい場合は、これらの記事が参考になるだろう。

- [図で理解するJavaScriptのプロトタイプチェーン - Qiita](https://qiita.com/howdy39/items/35729490b024ca295d6c)
- [applyとcallの使い方を丁寧に説明してみる - あと味](https://taiju.hatenablog.com/entry/20100515/1273903873)

このテクニックは array-like object を配列に変換するのによく用いられている。この他にも、文字列や `arguments` オブジェクトなどが array-like であるとよく言われる。

# MDN の説明

>Some JavaScript objects, such as the NodeList returned by document.getElementsByTagName() or the arguments object made available within the body of a function, look and behave like arrays on the surface but do not share all of their methods. The arguments object provides a length attribute but does not implement the forEach() method, for example.
([Indexed collections - JavaScript | MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Indexed_collections#Working_with_array-like_objects) より引用)

訳: 「`document.getElementsByTagName()` の返り値の `NodeList` や、関数内で使える `arguments` オブジェクトといった一部のオブジェクトは、表面上では配列のように振る舞うが、すべての配列メソッドを持っているわけではない。」

このように、配列と同じように振る舞う、ということしか書いておらず、正確な定義が分からない。

# ECMAScript の仕様での定義

![Array-like](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/446383/59e7b409-ac40-6043-45e2-9408c5ced4bd.png)


つまり array-like object とは **`length` プロパティの値が *ToLength* で長さに変換できる(失敗しない)オブジェクト**である。そして、以下のように *ToLength* は内部的に *ToNumber* を呼び出しており、**この *ToNumber* が失敗しなければ *ToLength* も失敗しない**ということが分かる。

![ToLength](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/446383/13a9d658-3756-5fb5-8835-45ff36d637df.png)

![ToInteger](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/446383/fa87678f-73c1-da4c-045f-7c9e31401834.png)

*ToNumber* は実は JavaScript の `Number()` 関数によっても使われるものである。詳しく説明しないが `new` なしで呼び出されると *NewTarget* は `undefined` になるため、`Number()` 関数の返り値は *ToNumber* の結果そのままである。

![Number()](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/446383/d3eaeb57-acc4-6bf2-bbce-71e13be66fd6.png)


以上から分かったことは、 **array-like object = 「`Number(obj.length)` で例外が発生しないようなオブジェクト `obj`」** である。

# ほとんどのオブジェクトは array-like ?

ということは、**空オブジェクト**でさえも `Number(obj.length)` は `Number(undefined)` つまり `NaN` となるため、**array-like となってしまう**のである。実際、`Number()` が例外を投げるケースは、以下のように引数が `Symbol` である場合か一部の `Object` の場合しかない。

![ToNumber](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/446383/464e12e2-cf0c-221d-bed4-115adaadc837.png)


`Object` の場合で例外が発生するのは、以下のような特殊なケースである。

```js
Number({ toString: null }) // Uncaught TypeError: Cannot convert object to primitive value
```

以上を踏まえて例をあげる。


**array-like object でない**例

```js
{ length: { toString: null } }
{ length: Symbol("mySymbol") }
null
undefined
```

**array-like object** の例

```js
{}
{ length: null }
{ length: 7 }
```

実際、以下のように配列メソッドを呼び出すことができる。

```js
Array.prototype.slice.call({})  // []
Array.prototype.slice.call({length:3})  // [empty, empty, empty]
```

Note: `1` や `true` といった値も「object である」という点以外では array-like object の条件を満たしている。

# iterable との違いは？

ここまで読めば、array-like は iterable とは全く異なるインターフェースであることが既に分かるだろう。iterable であるとは、`Symbol.iterator` メソッドをもち、それが iterator を返すことである。

```js
const arr = [1, 2, 3]
const iterator = arr[Symbol.iterator]()
```

iterator は呼び出されるたびに「次の値があるか(`done`)、あれば値は何か(`value`)」という情報を含むオブジェクトを返すことになっている。

```js
iterator.next()  // {value: 1, done: false}
iterator.next()  // {value: 2, done: false}
iterator.next()  // {value: 3, done: false}
iterator.next()  // {done: true}
```

iterable インターフェースは JavaScript のいろいろなところで使われており、スプレッド構文 `[...iterable]`、`fun(...iterable)` や for-of `for (const item of iterable)` などである。

iterable であるビルトインタイプは `Array`、`TypedArray`、`String`、`Map`、`Set` のみであり、当然自分で iterable を実装することもできる。

# まとめ

仕様に基づけば、`{}` のような全く配列らしくない値でさえも array-like であるということが分かった。しかし、一般的には "array-like" は 「`length` プロパティが数値でありかつ `arrLike[0]` のように要素にアクセスできる」という意味で使われることが多いので、それぞれ広義、狭義として覚えておくとよいだろう。