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


イントロ

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

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

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

例えば document.getElementsByClassName() の返り値は HTMLCollection であり、これは array-like である。HTMLCollection.slice() などの配列メソッドを持っていないが、array-like であるため以下のように強制的に配列メソッドを呼び出すことができる。

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

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

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

このテクニックは 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 より引用)

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

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

ECMAScript の仕様での定義

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

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

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

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

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

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

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

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

array-like object でない

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

array-like object の例

{}
{ length: null }
{ length: 7 }

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

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

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

iterable との違いは?

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

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

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

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 であるビルトインタイプは ArrayTypedArrayStringMapSet のみであり、当然自分で iterable を実装することもできる。

まとめ

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