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 Syntax によって展開することが出来ます。
ArrayLike について
ここでは ArrayLike の定義として length を持つオブジェクトを ArrayLike と呼ぶことにします1。例えば以下のようなオブジェクトは ArrayLike です。
{
length: 10
}
// 値を持つ場合
{
"0": 1,
"1": 12,
"2": 123,
length: 3
}
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;
};
(https://dom.spec.whatwg.org/#interface-nodelist より引用[^2])
一方で `HTMLCollection` は Iterable ではありません。
> ```idl
[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
};
(https://github.com/zloirock/core-js/blob/0f1c2bffe6e2e26b441908999b8b7469d1ab7a9c/modules/web.dom.iterable.js#L12-L44 より引用)
[^2]: Commit Snapshot: https://dom.spec.whatwg.org/commit-snapshots/fb6638fa3d02985e43782d8857edaa915d499261/
## W3C WebIDL における Iterable, ArrayLike
DOM API では特に Iterable の記載がされていない `HTMLCollection` でしたが、W3C WebIDL の `@@iterator`(`Symbol.iterator` のこと)の項目を見ると以下のように記されています。
>
If the [interface](https://heycam.github.io/webidl/#dfn-interface) has any of the following:
>
* an [iterable declaration](https://heycam.github.io/webidl/#dfn-iterable-declaration)
* an [indexed property getter](https://heycam.github.io/webidl/#dfn-indexed-property-getter) and an [integer-typed](https://heycam.github.io/webidl/#dfn-integer-type) [attribute](https://heycam.github.io/webidl/#dfn-attribute) named “length”
* a [maplike declaration](https://heycam.github.io/webidl/#dfn-maplike-declaration)
* a [setlike declaration](https://heycam.github.io/webidl/#dfn-setlike-declaration)
>
then a property must exist whose name is the [@@iterator](https://tc39.github.io/ecma262/#sec-well-known-symbols) symbol, with attributes { [[Writable]]: true, [[Enumerable]]: false, [[Configurable]]: true } and whose value is a [function object](https://tc39.github.io/ecma262/#function-object).
(https://heycam.github.io/webidl/#es-iterator より引用)
これによって **ArrayLike であって indexed property によって値を返すオブジェクトはすべて Iterable として実装しなければならない**ことになっています。
前述した core-js(babel-polyfill) でも区別はされているものの Iterable になるように実装されています[^3]。
[^3]: https://github.com/zloirock/core-js/#iterable-dom-collections
## ブラウザにおける実装
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 にする便利さはわかりますが、なんか後方互換のためにまた仕様が辛いことになってるなぁという印象ですね。