JavaScript | 第16章「イテレーターとジェネレーター」

javascrpit JavaScript
スポンサーリンク

「イテレーターとジェネレーター」をプログラミング初心者向けに噛み砕いて説明します。例をいっぱい入れて、実際に手を動かせるコード(ブラウザのコンソールでそのまま試せます)も載せます。まず要点をざっと押さえてから、具体例→実用例→練習問題、という流れで行きます。

ざっくり要点

  • イテレーター(iterator):順番に値を取り出す「読み出し器」。next() を呼ぶと { value: X, done: false } のように返してくれるオブジェクトのこと。
  • 反復可能(iterable)オブジェクトfor...of や展開(...)などで順に取り出せるもの。内部に Symbol.iterator メソッドを持ち、呼ぶとイテレーターを返すオブジェクト。配列や文字列は反復可能。
  • ジェネレーター(generator):簡単にイテレーターを作れる関数。function*yield を使う。呼ぶとイテレーター(=ジェネレーターオブジェクト)を返す。next()yield のところまで実行を進め、値を取り出す。
  • ジェネレーターは 「遅延評価」(値を必要になったときに作る)に便利。無限列や重い処理の逐次生成で威力を発揮。

1. イテレーターのしくみ

想像:本を一ページずつ読む → 「次のページをめくる」操作が next() に当たります。

イテレーターのルール(プロトコル)

  • next() を呼ぶと { value: 取り出した値, done: 真偽 } を返す。
    • done: false はまだ要素がある。
    • done: true は終わり(value は最後の値か undefined)。

簡単な手作りイテレーター(コード) — ブラウザのコンソールで実行してみてください:

function makeIterator(arr) {
  let i = 0;
  return {
    next() {
      if (i < arr.length) {
        return { value: arr[i++], done: false };
      } else {
        return { value: undefined, done: true };
      }
    }
  };
}

const it = makeIterator(['a','b','c']);
console.log(it.next()); // {value: "a", done: false}
console.log(it.next()); // {value: "b", done: false}
console.log(it.next()); // {value: "c", done: false}
console.log(it.next()); // {value: undefined, done: true}
JavaScript

2. 反復可能(iterable)オブジェクトと Symbol.iterator

配列は自動で反復可能([Symbol.iterator] を持つ)なので、for...of やスプレッドで使えます:

for (const x of ['x','y','z']) {
  console.log(x); // x, y, z
}
JavaScript

arr[Symbol.iterator]() を呼すと配列のイテレーター(上と同じ役割)を手に入れられます。for...of は内部でこのイテレーターを使っています。

手動で Symbol.iterator を実装すると、自分のオブジェクトを for...of で使えるようにできます:

const range = {
  from: 1,
  to: 3,
  [Symbol.iterator]() {
    let current = this.from;
    const last = this.to;
    return {
      next() {
        if (current <= last) {
          return { value: current++, done: false };
        } else {
          return { value: undefined, done: true };
        }
      }
    };
  }
};

for (const n of range) console.log(n); // 1, 2, 3
JavaScript

3. ジェネレーター(generator)を使うともっと簡単

ジェネレーターはイテレーターを簡単に書ける特別な関数です。function*yield を使います。呼ぶとイテレーター(=ジェネレーターオブジェクト)を返します。

例:range をジェネレーターで実装

function* range(from, to) {
  for (let i = from; i <= to; i++) {
    yield i; // ここで値を外に渡して、次に呼ばれるまで関数は一時停止
  }
}

const it = range(1,3);
console.log(it.next()); // {value:1, done:false}
console.log(it.next()); // {value:2, done:false}
console.log(it.next()); // {value:3, done:false}
console.log(it.next()); // {value:undefined, done:true}

// for...of でも使える
for (const x of range(4,6)) console.log(x); // 4,5,6
JavaScript

yield は「ここで値を返して一時停止する」命令。next() を呼ぶたびに次の yield まで実行が進みます。

4. ジェネレーターの便利機能(ちょっと上級)

  • yield*:他のイテレーター/ジェネレーターを委譲して全ての値を再配布できます(ネストを簡単に扱える)。
  • ジェネレーターオブジェクトの next(value) に値を渡すと、直前の yield 式の値として受け取れる(双方向のやり取り)。
  • return() を呼ぶとジェネレーターを途中で終了させることができる。
  • throw() を使うとジェネレーター内に例外を送れる(エラーハンドリング用途)。

例(yield* の簡単な例):

function* inner() { yield 'A'; yield 'B'; }
function* outer() { yield 'start'; yield* inner(); yield 'end'; }

for (const x of outer()) console.log(x); // start, A, B, end
JavaScript

5. どんな場面で使うと良い?(実用的ヒント)

  • 大きなデータを逐次処理:全部一気に読み込まず、必要になった分だけ処理してメモリ節約。
  • 無限列:(例:自然数、フィボナッチ)を作って必要なだけ取り出す。全部を配列にすると終わらない/重い。
  • 非同期処理との組合せasync ジェネレーター(async function*)はストリーミング的に非同期データを扱えます(別トピック)。

無限列の例(ジェネレーターで自然数列):

function* naturals() {
  let i = 1;
  while (true) yield i++;
}

const it = naturals();
console.log(it.next().value); // 1
console.log(it.next().value); // 2
// 必要なだけ繰り返せる(ただし止めどきは自分で制御)
JavaScript

6. よくある間違い・つまずきポイント

  • yield は値を返すが関数全体の戻り値ではないyield はその場で一回だけ返す。最後にジェネレーター全体を終えたときの戻り値は return で指定するか、done: truevalue を見る。
  • forEach では使えないforEach は配列専用で内部で Symbol.iterator を直接触らない場合があり、ジェネレーターのふるまいとは違う(for...of を使う)。
  • 同期ジェネレーターと非同期(async)ジェネレーターを混同しない:非同期ストリームを扱うには async function*for await...of を学ぶ必要がある。

7 練習問題

  1. 数字の配列 [10,20,30]Symbol.iterator() を取り出して next() を手で3回呼んでみよう。何が返る?
  2. function* squares(n) を作って、1..n の平方(1,4,9,…)を yield するジェネレーターを作ろう。for...of で出力してみて。
  3. ジェネレーターを使って、フィボナッチ数列(1,1,2,3,5,8…)を作り、最初の10個を出力してみよう。
タイトルとURLをコピーしました