ParentNode.childrenをfor...ofループするときの注意点


  • for (const el of parentElement.children) は動かないものと考える
    • 原因と対策について解説

ES2015 おさらい

for...of ループ

  • iterable なオブジェクトを1件ずつ処理するためのループ構文
    • for each 的なやつ
var elements = document.querySelectorAll('.item');

// for...in だと hasOwnProperty とかややこしい
for (const i in elements) {
  if (elements.hasOwnProperty(i)) {
    const el = elements[i];

    ...
  }
}

// forEach() メソッドは Array にしかない
//elements.forEach((el) => { ... });
Array.prototype.forEach.call(elements, (el) => {
  ...
});

// そこで for...of
for (const el of elements) {
  ...
}

iterable

  • for...of 文に渡すことができるオブジェクトのプロトコル (インターフェイスのようなもの)
    • String, Array, Map, Set などのループできそうなオブジェクトが該当
    • [Symbol.iterator] メソッドを呼び出すと iterator オブジェクトが取得できる
const items = ['one', 'two', 'three'];
const iter = items[Symbol.iterator]();

console.log(iter.next());  // { value: "one", done: false }
console.log(iter.next());  // { value: "two", done: false }
console.log(iter.next());  // { value: "three", done: false }
console.log(iter.next());  // { value: undefined, done: true }

NodeList と HTMLCollection

  • document.querySelectorAll() などの戻り値は NodeList オブジェクト
  • ParentElement.children の戻り値は HTMLCollection オブジェクト
  • どちらも似たような、DOM要素の配列
  • NodeList は iterable だが、 HTMLCollectioniterable ではない
    • たとえば下記のコードを Babel with babel-polyfill で ES2015 にトランスパイルして動作させた場合、 HTMLCollection[Symbol.iterator]() がないため エラーになる
// 仕様上は動作しない
// (ただ Google Chrome だと HTMLCollection も iterable 化されてるので動く)
for (const el of parentElement.children) {
  ...
}

対策

  • Array に変換するのが手っ取り早い
// とりあえず Array に変換
for (const el of Array.from(parentElement.children)) {
  ...
}

// だったら forEach でもいいや感 key/value つかえるメリットはある
Array.from(parentElement.children).forEach((el) => {
  ...
});
  • どうしてもやりたいなら HTMLCollection のプロトタイプを書き換えて iterable に
if (HTMLCollection.prototype[Symbol.iterator] === undefined) {
  HTMLCollection.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];
}

まあ DOM を直でさわらない時代だし、雑学的に知っておくとよいかもくらいな話です。

参考文献