TL; DR
DOM API 的には HTMLCollection
は Iterable となっていないが、 W3C WebIDL 的には Iterable になっている。
Chrome, Firefox では Iterable として実装されており、Safari は 11 から Iterable になっている。また core-js(babel-polyfill) でも Iterable になる。
一方で Edge 40 では HTMLCollection
は Iterable になっていない。
闇。
イントロダクション
JavaScript の言語仕様である ECMAScript ではオブジェクトの分類として Iterable と ArrayLike があります。
いずれも Array.from
で Array
に変換することが出来ます。
Iterable について
Iterable は Symbol.iterator
メソッドを実行すると Iterator を返すオブジェクトのことをいいます。例えば以下のようなオブジェクトは Iterable です。
{
[Symbol.iterator]() {
const values = [1, 12, 123];
let index = 0;
return {
next() {
const value = values[index++];
return { value, done: value === undefined };
}
}
}
}
// Generator Functions を使うともっと簡単に書ける
{
* [Symbol.iterator]() {
yield* [1, 12, 123];
}
}
JavaScript の イテレータ を極める! がとてもわかりやすい記事なので、詳しくはそちらを御覧ください。
Iterable は for...of
や Spread Operator によって展開することが出来ます。
ArrayLike について
ここでは ArrayLike の定義として length を持つオブジェクトを ArrayLike と呼ぶことにします1。例えば以下のようなオブジェクトは ArrayLike です。
{
length: 10
}
ECMAScript における Iterable, ArrayLike
Array
は Iterable と ArrayLike の特徴を両方とも持っているので、Iterable かつ ArrayLike とみなすことが出来ます。
const array = [1, 12, 123];
// Iterable の特徴を持つ
typeof array[Symbol.iterator] === "function"; // true
// ArrayLike の特徴を持つ
array.length !== undefined; // true
一方で Array#keys
, Array#values
, Array#entries
が返す ArrayIterator
は Itarable ではあるものの ArrayLike ではありません。
const arrayIterator = [1, 12, 123].values();
// Iterable の特徴を持つ
typeof arrayIterator[Symbol.iterator] === "function"; // true
// ArrayLike の特徴を持たない
arrayIterator.length !== undefined; // false
DOM API における Iterable, ArrayLike
DOM API では仕様策定の手続きに時間がかかることによってか ArrayLike であるものの Iterable ではないオブジェクトが大量に取り残されています。
NodeList
については WHATWG と W3C の両方で Iterable ということになっています。
[Exposed=Window] interface NodeList { getter Node? item(unsigned long index); readonly attribute unsigned long length; iterable<Node>; };
一方で HTMLCollection
は Iterable ではありません。
[Exposed=Window, LegacyUnenumerableNamedProperties] interface HTMLCollection { readonly attribute unsigned long length; getter Element? item(unsigned long index); getter Element? namedItem(DOMString name); };(https://dom.spec.whatwg.org/#interface-htmlcollection より引用2)
これらについては babel-polyfill で使われている core-js でも区別されています。
var DOMIterables = { CSSRuleList: true, // TODO: Not spec compliant, should be false. CSSStyleDeclaration: false, CSSValueList: false, ClientRectList: false, DOMRectList: false, DOMStringList: false, DOMTokenList: true, DataTransferItemList: false, FileList: false, HTMLAllCollection: false, HTMLCollection: false, HTMLFormElement: false, HTMLSelectElement: false, MediaList: true, // TODO: Not spec compliant, should be false. MimeTypeArray: false, NamedNodeMap: false, NodeList: true, PaintRequestList: false, Plugin: false, PluginArray: false, SVGLengthList: false, SVGNumberList: false, SVGPathSegList: false, SVGPointList: false, SVGStringList: false, SVGTransformList: false, SourceBufferList: false, StyleSheetList: true, // TODO: Not spec compliant, should be false. TextTrackCueList: false, TextTrackList: false, TouchList: false };
W3C WebIDL における Iterable, ArrayLike
DOM API では特に Iterable の記載がされていない HTMLCollection
でしたが、W3C WebIDL の @@iterator
(Symbol.iterator
のこと)の項目を見ると以下のように記されています。
If the interface has any of the following:
- an iterable declaration
- an indexed property getter and an integer-typed attribute named “length”
- a maplike declaration
- a setlike declaration
then a property must exist whose name is the @@iterator symbol, with attributes { [[Writable]]: true, [[Enumerable]]: false, [[Configurable]]: true } and whose value is a function object.
(https://heycam.github.io/webidl/#es-iterator より引用)
これによって ArrayLike であって indexed property によって値を返すオブジェクトはすべて Iterable として実装しなければならないことになっています。
前述した core-js(babel-polyfill) でも区別はされているものの Iterable になるように実装されています3。
ブラウザにおける実装
Chrome, Firefox では WebIDL の仕様に踏襲して HTMLCollection
が Iterable として実装されています。
また Safari は最新の Safari 11 から Iterable となっています。
一方で Edge 40 では Iterable になっていないようです。
思ったこと
WebIDL の indexed property によって Iterable にしてしまうのはいいと思います。
FileList
みたいに Iterable であって然るべきものがなかなか Iterable にならないというのは辛いものがあります。
DOM API 側で特に Iterable という記載がしていないのに WebIDL 側で上書きして Iterable っていうことにするのは困惑するからやめろ。
しかし HTMLCollection
は interface を見るとわかりますが indexed property の他にも named property を持っています。この named property を無視して Iterable にしてしまうのはいいんでしょうか。まあこの named property はもう殆ど使われていない感じではありますが。
Iterable にする便利さはわかりますが、なんか後方互換のためにまた仕様が辛いことになってるなぁという印象ですね。
-
厳密に考え始めると ToLength に length を渡すことで、返り値を取得できるオブジェクトが ArrayLike なオブジェクトとみなすことが出来ますが、この場合だと length プロパティを持っていなくても
0
を返してしまったり、length プロパティにSymbol
を持っている場合はTypeError
を投げることからややこしくなるためこのような定義としました。 ↩ -
Commit Snapshot: https://dom.spec.whatwg.org/commit-snapshots/fb6638fa3d02985e43782d8857edaa915d499261/ ↩
-
https://github.com/zloirock/core-js/#iterable-dom-collections ↩