HTMLCollection は Iterable なのか?

  • 4
    Like
  • 0
    Comment

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.fromArray に変換することが出来ます。

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>;
};

https://dom.spec.whatwg.org/#interface-nodelist より引用2

一方で 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
};

https://github.com/zloirock/core-js/blob/0f1c2bffe6e2e26b441908999b8b7469d1ab7a9c/modules/web.dom.iterable.js#L12-L44 より引用)

W3C WebIDL における Iterable, ArrayLike

DOM API では特に Iterable の記載がされていない HTMLCollection でしたが、W3C WebIDL の @@iteratorSymbol.iterator のこと)の項目を見ると以下のように記されています。

If the interface has any of the following:

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 にする便利さはわかりますが、なんか後方互換のためにまた仕様が辛いことになってるなぁという印象ですね。


  1. 厳密に考え始めると ToLength に length を渡すことで、返り値を取得できるオブジェクトが ArrayLike なオブジェクトとみなすことが出来ますが、この場合だと length プロパティを持っていなくても 0 を返してしまったり、length プロパティに Symbol を持っている場合は TypeError を投げることからややこしくなるためこのような定義としました。 

  2. Commit Snapshot: https://dom.spec.whatwg.org/commit-snapshots/fb6638fa3d02985e43782d8857edaa915d499261/ 

  3. https://github.com/zloirock/core-js/#iterable-dom-collections