JavaScript | ES6+ 文法:その他の ES6+ 機能 – for…of の内部

JavaScript JavaScript
スポンサーリンク

for…of の「見た目」と「中身」は別物だと思ってみる

for...of は、表面上はとてもシンプルです。

const arr = [10, 20, 30];

for (const value of arr) {
  console.log(value);
}
// 10
// 20
// 30
JavaScript

「配列の中身を順番に取り出してくれる便利な構文」に見えますが、
実は裏側でかなりキッチリした仕組みが動いています。

その仕組みのキーワードが次のふたつです。

Iterable(イテラブル)
Iterator(イテレータ)

これらを理解すると、for...of
「魔法の構文」ではなく「イテレータを自動で回してくれる糖衣構文」に見えてきます。

ここが重要です。
for…of は「配列専用の文法」ではなく、「イテラブルなものなら何でも回せる“汎用のループ構文”」です。
この「イテラブルを回す」という目線が持てるかどうかが、理解の分かれ目です。

for…of が内部でやっている基本ステップ

まずは [Symbol.iterator] を呼び出す

for…of が最初にやることは、「対象がイテラブルかどうか」の確認です。

配列、文字列、Map、Set などは、すべて
[Symbol.iterator] という特別なメソッドを持っています。

const arr = [10, 20, 30];

const iterator = arr[Symbol.iterator]();  // これは手動でやっているだけ

console.log(typeof iterator.next);        // "function"
JavaScript

[Symbol.iterator]() を呼び出した結果が、「イテレータ」です。
イテレータは必ず next() メソッドを持っています。

次に next() を繰り返し呼び出す

イテレータの next() を呼ぶと、
必ず { value: 何か, done: true/false } というオブジェクトが返ってきます。

const iterator = arr[Symbol.iterator]();

console.log(iterator.next()); // { value: 10, done: false }
console.log(iterator.next()); // { value: 20, done: false }
console.log(iterator.next()); // { value: 30, done: false }
console.log(iterator.next()); // { value: undefined, done: true }
JavaScript

done: false の間は、まだ値が残っています。
done: true が返ってきたら「もう終わり」です。

for…of はこの処理を自動で書いてくれている

つまり、次の二つは同じことをしています。

for…of を使ったコード:

for (const value of arr) {
  console.log(value);
}
JavaScript

内部的にやっていそうな処理を手で書くとこうなります:

const iterator = arr[Symbol.iterator]();

while (true) {
  const result = iterator.next();

  if (result.done) {
    break;
  }

  const value = result.value;
  console.log(value);
}
JavaScript

これが for…of の正体です。
「イテレータを取得 → next() を何度も呼ぶ → done: true で終了」
この一連の流れを、自動でやってくれる構文なのです。

ここが重要です。
for…of を見るとき、「内部で iterator.next() が回っている」とイメージできるかどうか。
これが分かると、Iterator / Generator とのつながりが一気に見えてきます。

for…of が使える条件:「イテラブル」であること

イテラブルとは「[Symbol.iterator] を持っているもの」

配列や文字列が for…of で回せるのは、
「たまたま配列だから」ではなく、「イテラブルだから」です。

イテラブルの条件はとてもシンプルで、
obj[Symbol.iterator] が「イテレータを返す関数」であることです。

配列で試してみます。

const arr = [1, 2, 3];

console.log(typeof arr[Symbol.iterator]); // "function"

const it = arr[Symbol.iterator]();
console.log(it.next()); // { value: 1, done: false }
JavaScript

文字列も同じです。

const str = "Hi";

console.log(typeof str[Symbol.iterator]); // "function"

const it2 = str[Symbol.iterator]();
console.log(it2.next()); // { value: "H", done: false }
JavaScript

自作オブジェクトを for…of 対応にしてみる

例えば、「1 から n まで数えるオブジェクト」を自分で作ってみましょう。

const counter = {
  start: 1,
  end: 5,
  [Symbol.iterator]() {
    let current = this.start;
    const end = this.end;

    return {
      next() {
        if (current <= end) {
          return { value: current++, done: false };
        } else {
          return { value: undefined, done: true };
        }
      },
    };
  },
};

for (const num of counter) {
  console.log(num);
}
// 1 2 3 4 5
JavaScript

このオブジェクトは配列でも Map でもありません。
でも [Symbol.iterator] を実装したことで、
for…of が「イテラブル」と認識してくれます。

ここが重要です。
for…of が回せるかどうかは、「配列かどうか」ではなく
「イテレータを返す [Symbol.iterator] を持っているかどうか」で決まる。
この視点を持てると、for…of が一気に“言語のルール”として理解できます。

break / continue / return とイテレータの関係

break したときのイメージ

for…of を途中で break すると、
「裏側の while ループを途中で抜ける」と考えられます。

const arr = [1, 2, 3, 4, 5];

for (const n of arr) {
  if (n > 3) break;
  console.log(n);
}
// 1 2 3
JavaScript

内部イメージにすると、

const it = arr[Symbol.iterator]();

while (true) {
  const { value, done } = it.next();
  if (done) break;

  if (value > 3) break;
  console.log(value);
}
JavaScript

という感じです。

基本的な配列や文字列のイテレータでは、
特別な後処理はありませんが、
自作のイテレータや一部のライブラリでは、
「ループが最後まで行かなかったときに cleanup をしたい」ということもあります。

return(関数ごと抜ける)も同じように途中終了

for…of の中で return すると、
当然ループも途中で終了します。

function findFirstEven(arr) {
  for (const n of arr) {
    if (n % 2 === 0) {
      return n;
    }
  }
  return null;
}

console.log(findFirstEven([1, 3, 4, 6])); // 4
JavaScript

内部イメージ的には、「next() を途中までしか呼ばないでループを抜けた」という状態になります。

Generator と for…of の組み合わせ

Generator(function*)は、それ自体が
「イテレータでもあり、イテラブルでもある」オブジェクトを返します。

function* gen() {
  yield 1;
  yield 2;
  yield 3;
}

for (const v of gen()) {
  console.log(v);
}
// 1 2 3
JavaScript

for…of はここでも、
gen() が返すオブジェクトから [Symbol.iterator] を呼び出し、
next() を繰り返しているだけです。

Generator の章でやったことが、そのまま for…of に直結しているのが分かると思います。

for…in と for…of の違い(内部的な意味の違い)

for…in は「キー(プロパティ名)用」

const obj = { a: 1, b: 2 };

for (const key in obj) {
  console.log(key);      // "a", "b"
  console.log(obj[key]); // 1, 2
}
JavaScript

for…in は「プロパティ名(キー)」を列挙するための構文です。
対象はオブジェクトで、
内部的には in 演算子とプロトタイプチェーンのルールに従ってキーを列挙します。

配列に使うと、「インデックス(”0″, “1”, … の文字列)」が取れてくるので、
通常は配列には向きません。

for…of は「値用」、イテラブルが対象

const arr = [10, 20, 30];

for (const value of arr) {
  console.log(value); // 10, 20, 30
}
JavaScript

for…of は、「イテラブル([Symbol.iterator] を持つもの)」から
「値」を順番に取り出すための構文です。

内部では iterator.next() を呼び続けているだけなので、
オブジェクト {} のようにイテレータを持たないものには使えません。

ここが重要です。
for…in は「キー列挙」、for…of は「イテレータを回して値列挙」。
まったく別の仕組みで動いている構文だということを、混同しないようにしてください。

まとめ:for…of の内部モデルを一文で言うと

for…of は、内部的には

対象Symbol.iterator でイテレータを取得し、
その iterator.next() を繰り返し呼びながら、
done: true になるまで value を順番に取り出す構文

です。

押さえておきたいポイントを整理すると、次のようになります。

for…of が回せるのは、「[Symbol.iterator] を持つイテラブル」である
配列・文字列・Map・Set・Generator などは、みんなこのルールに従っている
for…of は、裏で iterator.next() を呼んで { value, done } を見ているだけ
自作オブジェクトに [Symbol.iterator] を実装すれば、それも for…of 対応にできる
for…in は「キー列挙」、for…of は「イテレータによる値列挙」と役割が違う

最後に、練習としてこうしてみてください。

まず普通に for…of を書き、そのあとに
「この for…of は内部的にはこうなってるはず」と思って
[Symbol.iterator]()next() を自分の手で呼び出すコードを書いてみる。

それを一度でもやると、
for…of の「中にいるイテレータの姿」が、かなりはっきり見えてくるはずです。

タイトルとURLをコピーしました