JavaScript中級 #3 イテレータとジェネレータ

#2 非同期入門に続いて、今回はJavaScriptのもう一つの中核ツール — イテレータジェネレータ

for...of はどう動作するのか? #

基礎の#3 制御フローfor...of で配列を巡回しました。

for...of もう一度見る
for (const x of [1, 2, 3]) {
  console.log(x);
}

これは配列だけで動作するのではありません。JavaScriptはオブジェクトが一定の約束(イテラブルプロトコル)に従えば、すべて for...of で巡回できるように作っておきました。

すでにイテラブルなもの
for (const ch of 'hello') {       // 文字列
  console.log(ch);
}

for (const [k, v] of new Map([['a', 1], ['b', 2]])) {   // Map
  console.log(k, v);
}

for (const x of new Set([1, 2, 3])) {   // Set
  console.log(x);
}

配列、文字列、Map、Set、そして NodeList(document.querySelectorAll)などはすでにイテラブルです。すべて同じ約束に従います。

イテラブルプロトコル #

オブジェクトがイテラブルであるためには、次の一つを持っている必要があります。

Symbol.iterator というキーに関数があること。 その関数を呼び出すと next() を持ったイテレータを返すこと。 next(){ value, done } オブジェクトを返すこと。

直接作ってみると理解が早いです。

イテラブルを直接作る
const range = {
  from: 1,
  to: 5,
  [Symbol.iterator]() {
    let current = this.from;
    const last = this.to;
    return {
      next() {
        if (current <= last) {
          return { value: current++, done: false };
        }
        return { value: undefined, done: true };
      },
    };
  },
};

for (const n of range) {
  console.log(n);
}
// 1, 2, 3, 4, 5

Symbol.iterator が核心です。キーの位置に [...] が入っているのは基礎 #5の計算済みプロパティ構文です。Symbol.iterator という特別なシンボルをキーとして使います。

for...of は内部的にこの約束に従って next() を繰り返し呼び出しながら donetrue になるまで値を取り出します。

Spread とデストラクチャリングもイテラブル #

for...of のほかに spread と配列デストラクチャリングもイテラブルプロトコルを使います。

イテラブル活用
const arr = [...range];
// [1, 2, 3, 4, 5]

const [first, second] = range;
// first = 1, second = 2

Math.max(...range);   // 5
Array.from(range);    // [1, 2, 3, 4, 5]

これが強力です — 一度イテラブルにすればJavaScriptのすべてのシーケンスツールをそのまま使えます。

ジェネレータ — イテラブルを短く作る構文 #

上で作った range は動きますが、イテラブルを作るコードが長く複雑です。JavaScriptには同じことを一度にしてくれる構文があります — ジェネレータ

ジェネレータで range
function* range(from, to) {
  for (let n = from; n <= to; n++) {
    yield n;
  }
}

for (const n of range(1, 5)) {
  console.log(n);
}
// 1, 2, 3, 4, 5

[...range(1, 5)];   // [1, 2, 3, 4, 5]

構文:

  • function* — アスタリスクが付いた関数 → ジェネレータ
  • yield — 一つの値を出して止まる
  • 再び呼ばれると yield の次の行から続く

ジェネレータ関数を呼び出すとイテラブルなオブジェクトが返されます。for...of がそのオブジェクトから値を一つずつ取り出します。

遅延シーケンス — 無限シーケンスも可能 #

ジェネレータの真の強みは必要な分だけ作る点です。無限シーケンスも表現可能です。

無限自然数
function* naturals() {
  let n = 1;
  while (true) {
    yield n++;
  }
}

const gen = naturals();
gen.next();   // { value: 1, done: false }
gen.next();   // { value: 2, done: false }
gen.next();   // { value: 3, done: false }
// ... 永遠に

while (true) が無限ループですが止まりません。next() を呼び出すたびに次の値だけを作ります。呼び出し時点でだけ計算 — これが遅延(lazy)の意味です。

無限シーケンスを切って使う #

最初の N 個だけ
function* take(iterable, n) {
  let count = 0;
  for (const x of iterable) {
    if (count >= n) return;
    yield x;
    count++;
  }
}

const first10 = [...take(naturals(), 10)];
// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

これが関数型ライブラリがよく提供する take 関数の正体です。

ジェネレータの他の活用 #

1) ツリー/グラフの巡回 #

ツリー巡回
function* walk(node) {
  yield node.value;
  for (const child of node.children ?? []) {
    yield* walk(child);    // 別のジェネレータに委譲
  }
}

const tree = {
  value: 'root',
  children: [
    { value: 'a', children: [{ value: 'a1' }] },
    { value: 'b' },
  ],
};

for (const v of walk(tree)) {
  console.log(v);
}
// root, a, a1, b

yield*他のイテラブルの値をまるごと流し送る構文。再帰的な巡回をすっきりと表現できます。

2) 段階的に進む作業 #

多段階作業
function* steps() {
  console.log('ステップ1 開始');
  yield;
  console.log('ステップ2 開始');
  yield;
  console.log('ステップ3 終了');
}

const s = steps();
s.next();   // ステップ1 開始
s.next();   // ステップ2 開始
s.next();   // ステップ3 終了

呼び出し側が明示的に next() を呼びながら段階を進めます。テストやシミュレーションの場面でしばしば活用されます。

非同期イテレータ — for await...of #

非同期データを巡回するときに使う変形。ストリーミングデータ(ファイル、fetch レスポンスの chunk)を扱うときに出会います。

for await...of
async function readChunks(response) {
  for await (const chunk of response.body) {
    console.log(chunk);
  }
}

詳しい使用は次の#6 fetch APIで扱います。

Symbol とは何か? #

Symbol.iterator が突然登場しましたが、Symbol もJavaScriptのプリミティブ型の一つです。ユニークな識別子を作ってくれる値です。

Symbol 基本
const s1 = Symbol('id');
const s2 = Symbol('id');

s1 === s2;   // false — 同じ説明でも別のシンボル

言語レベルのキー(Symbol.iteratorSymbol.asyncIterator など)は絶対に衝突しないようにするため Symbol で定義されています。一般的なコードでは頻繁に作ることはありませんが、ライブラリがメタデータをオブジェクトに付けるときに衝突なしで使う場面が時々あります。

まとめ #

この記事で整理した内容:

  • for...of / spread / デストラクチャリングはイテラブルプロトコル上で動作
  • イテラブルになるには Symbol.iteratornext() を持ったイテレータを返す
  • ジェネレータ(function*yield)でイテラブルを短く作る
  • 遅延シーケンス — 無限シーケンスも表現可能
  • yield* で他のイテラブルへ委譲
  • for await...of で非同期イテラブルを巡回
  • Symbol は衝突しないユニークな識別子

次の記事(#4 デストラクチャリングと spread/rest 詳細)では、基礎で軽く扱ったデストラクチャリングと spread をより深く — ネストパターン、引数デストラクチャリング、動的キーなどよく出会う実戦パターンを整理します。

X