TL

ジェネレータとイテレータプロトコルの内部動作

for-of やスプレッドが内部で何を呼んでいるかが分かると、独自オブジェクトを自在に反復させられる。yield の中断・再開とイテレータプロトコルの仕組みを内部動作から解説します。

応用JavaScriptジェネレータイテレータ非同期ブラウザ最終更新: 2026-06-21
TL;DR要点だけ先に
  • 1.イテレータは next() を呼ぶたびに { value, done } を返すオブジェクト。for-of・スプレッド・分割代入は Symbol.iterator でこのイテレータを取り出し、done が true になるまで next() を回す。
  • 2.ジェネレータ関数は呼んでも本体を実行せず、停止状態のジェネレータ(イテレータ兼イテラブル)を返す。yield で実行を中断し、ローカル変数とプログラムカウンタを保持したまま、次の next() で同じ位置から再開する。
  • 3.next(v) の引数は直前の yield 式の評価結果になり、return() は途中終了、throw() は yield 位置で例外を注入する。for-of は中断時に必ず return() を呼ぶため、ジェネレータの finally でクリーンアップが走る。

反復を支える2つの約束事

JavaScript で配列・文字列・MapSetfor-of で回せたり、スプレッド([...x])で展開できたりするのは、それらが共通のプロトコルに従っているからです。この約束事は2層に分かれます。

  • イテレータプロトコルnext() メソッドを持ち、呼ぶたびに {value, done} という形のオブジェクトを返すこと。donetrue になったら反復終了の合図です。
  • イテラブルプロトコルSymbol.iterator という名前のメソッドを持ち、それを呼ぶとイテレータを返すこと。

この2つは仕様(ECMAScript)で厳密に定義された手続きで、for-of もスプレッドも分割代入も、すべて内部でこの手続きを呼び出します。基礎は JavaScript を前提に、本記事はその内部動作を解きほぐします。

プロトコル必須メソッド返すもの役割
イテラブル[Symbol.iterator]()イテレータ反復の開始点を提供する
イテレータnext(){ value, done }1要素ずつ前進する
イテレータ(任意)return() / throw(){ value, done }途中終了・例外注入

イテレータを手で書いてみる

イテレータの実体は、ただのオブジェクトです。状態を閉じ込めて next() で1歩ずつ進めれば、それだけで for-of の対象になります。

function range(start, end) {
  let i = start;
  return {
    // 自分自身をイテレータとして返す=イテラブルでもある
    [Symbol.iterator]() { return this; },
    next() {
      if (i < end) return { value: i++, done: false };
      return { value: undefined, done: true };
    },
  };
}

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

ポイントは done: false のときの value が「今回の要素」で、done: true になった瞬間の value反復値に含まれないことです。for-ofdonetrue になった時点でループを抜け、そのときの value は無視します(後述の return 文の戻り値だけは別扱い)。

イテレータとイテラブルは別概念

Map や配列は「イテラブル」で、Symbol.iterator を呼ぶと毎回新しいイテレータを返します。だから同じ配列を for-of で何度でも回せます。一方、イテレータ自身は使い切ると done: true を返し続ける1回限りの存在です。上の range のように [Symbol.iterator]() return this とすると「イテレータ兼イテラブル」になりますが、これは一度しか回せない点に注意します。

ジェネレータ:中断と再開のからくり

イテレータを手書きすると状態管理が面倒です。ジェネレータ関数function*)はこの定型を言語機能に押し込みます。最大の特徴は、関数の途中で実行を止め、後から同じ位置から再開できることです。

function* gen() {
  console.log('A');
  const x = yield 1;   // ここで中断。next() が呼ばれるまで止まる
  console.log('B', x);
  yield 2;
  console.log('C');
}

const it = gen();      // 本体はまだ1行も実行されない
it.next();             // 'A' → { value: 1, done: false } で停止
it.next(10);           // 'B 10' → { value: 2, done: false } で停止
it.next();             // 'C' → { value: undefined, done: true }

ここで起きていることを内部レベルで言うと、ジェネレータ関数の呼び出しは本体を実行せず、停止状態のジェネレータオブジェクトを生成して即座に返すだけです。next() を呼んで初めて本体が動き、yield 式に到達すると、エンジンはそのジェネレータの実行コンテキスト(ローカル変数・どこまで進んだかのプログラムカウンタ)をまるごと退避して呼び出し側に戻ります。次の next() で退避したコンテキストを復元し、yield直後から続きを実行します。コールスタックを丸ごと巻き取って後で巻き戻す、いわば中断可能な関数です。

next(v) の引数は yield の“返り値”になる

const x = yield 1x には、次の next(10) に渡した 10 が入ります。yield 1 自体は「1 を外へ渡して中断する」式で、その評価結果は再開時に外から注入される値です。だから最初の next() の引数は捨てられます(本体がまだ最初の yield に達していないため)。この双方向のやり取りが、ジェネレータを単なる値の列以上のもの(協調的なコルーチン)にしています。

return() と throw():途中終了と例外注入

イテレータには next() のほかに、任意の2つのメソッドがあります。ジェネレータはこれらを自動で実装します。

  • return(v):反復を途中で打ち切ります。ジェネレータ内では、現在の yield 位置に return v 文があったかのように振る舞い、finally ブロックがあれば実行してから {value: v, done: true} を返します。
  • throw(e):現在の yield 位置で例外 e を投げます。ジェネレータ内の try/catch で捕捉でき、捕まえなければ呼び出し側まで伝播します。
function* withCleanup() {
  try {
    yield 'open';
    yield 'work';
  } finally {
    console.log('cleanup'); // 途中終了でも必ず走る
  }
}

const it = withCleanup();
it.next();            // { value: 'open', done: false }
it.return('stop');    // 'cleanup' → { value: 'stop', done: true }
it.next();            // { value: undefined, done: true }(以降は終了済み)

この finally 保証が実務上きわめて重要です。ファイルハンドルや購読の解放を finally に置けば、反復が最後まで回ろうと途中で打ち切られようと確実にクリーンアップされます。次節の通り、for-of はこの return() を自動で呼んでくれます。

for-of・スプレッドが内部で呼ぶ手続き

for-of は糖衣構文で、脱糖(desugar)すると次の手続きそのものです。仕様の用語では、開始時に GetIterator[Symbol.iterator]() の呼び出し)、各周回で IteratorStepnext() を呼んで done を判定)、そして中断時に IteratorClosereturn() の呼び出し)を実行します。

// for (const v of iterable) { body } は概ねこう展開される
const it = iterable[Symbol.iterator]();   // GetIterator
try {
  while (true) {
    const r = it.next();                  // IteratorStep
    if (r.done) break;
    const v = r.value;
    /* body */
  }
} finally {
  // break / return / throw で抜けたときだけ呼ばれる
  if (!正常完了 && it.return) it.return(); // IteratorClose
}

重要なのは、ループを break したり例外で抜けたりした場合、for-ofreturn() を自動で呼ぶ点です。これがジェネレータの finally を発火させます。同じ仕組みはスプレッド([...it]f(...it))・分割代入([a, b] = it)・yield*Array.fromPromise.all などすべての反復で共通です。たとえば分割代入 [a, b] = it は2要素を取り出した時点で残りを使わないので、return() を呼んでイテレータを閉じます。

生の next() ループは return() を呼ばない

自分で while を書いて手動 next() するコードは、途中で抜けても return()呼びません。ジェネレータの finally が走らず、リソースが解放されない可能性があります。中断しうる反復を手書きするなら、抜けるパスで明示的に it.return && it.return() を呼ぶか、return() を自動で呼んでくれる for-of を使うのが安全です。

yield* による委譲

yield* は「別のイテラブルへ反復を丸ごと委譲する」演算子です。単なるループの省略ではなく、next()return()throw() の透過的な転送まで含みます。

function* inner() { yield 1; yield 2; }
function* outer() {
  yield 0;
  const result = yield* inner(); // inner を全消化。inner の return 値が result に入る
  yield 3;
}
[...outer()]; // [0, 1, 2, 3]

yield* inner() は内部で inner() のイテレータを取り出し、外から来た next(v) の値を内側へ中継し、内側の yield をそのまま外へ通します。さらに外側に対する return()/throw() も内側へ転送するため、ネストしたジェネレータでもクリーンアップと例外の連鎖が崩れません。

試験・面接の頻出ポイント

押さえる4点。❶ for-of[Symbol.iterator]() でイテレータを取り、donetrue まで next() を回す。❷ ジェネレータ関数は呼んだだけでは本体を実行せず、停止状態のオブジェクトを返す。❸ next(v) の引数は直前の yield 式の評価値になる(双方向通信)。❹ for-of・スプレッド・分割代入は中断時に return() を呼ぶので、ジェネレータの finally が走る。この4点でほとんどの挙動を説明できます。

遅延評価と無限列

ジェネレータの真価は、値を要求された瞬間に1個ずつ計算する遅延評価にあります。配列のように全要素を先に作る必要がないため、無限に続く列も表現できます。

function* naturals() {
  let n = 0;
  while (true) yield n++; // 無限。next() のたびに1個だけ計算
}

const it = naturals();
it.next().value; // 0
it.next().value; // 1(必要な分だけ評価される)

for-of でこのまま回すと止まらないので、take(n) のような上限つきの反復子と組み合わせて使います。メモリに全列を載せずに済むのが利点で、大きなデータのストリーミング処理やパイプライン構築に向きます。エンジンが yield ごとに実行コンテキストを退避・復元する仕組みは JavaScript エンジンの内部(JIT とインラインキャッシュ) で扱う最適化とも関わり、for-await-of を支える非同期イテレータは イベントループの内部構造 のマイクロタスクと連動します。

まとめ

まとめ

反復はイテラブル(Symbol.iterator を持つ)とイテレータ(next(){value, done} を返す)の2層プロトコルで成り立ち、for-of・スプレッド・分割代入は内部でこの手続きを呼びます。ジェネレータ関数は呼んでも本体を実行せず停止状態のオブジェクトを返し、yield で実行コンテキストを退避して中断、次の next() で同じ位置から再開します。next(v) の引数は yield 式の評価値となり、return()/throw() で途中終了や例外注入ができます。for-of が中断時に return() を呼ぶため finally が確実に走る点を押さえれば、安全な反復処理と遅延評価を自在に組めます。

Web/フロントエンド Article

ジェネレータとイテレータプロトコルの内部動作を実務で読む

TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。

解決すること

JavaScript

比較で見る軸

難易度: advanced / カテゴリ: Web/フロントエンド / タグ数: 5

導入後に効く点

ジェネレータ関数は呼んでも本体を実行せず、停止状態のジェネレータ(イテレータ兼イテラブル)を返す。yield で実行を中断し、ローカル変数とプログラムカウンタを保持したまま、次の next() で同じ位置から再開する。

先に潰すリスク

用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。

数字・仕様の読み方
難易度
advanced
カテゴリ
Web/フロントエンド
タグ数
5

判断チェックリスト

  • 自社の用途が「JavaScript / ジェネレータ」に近いか確認する。
  • 強みである「イテレータは next() を呼ぶたびに { value, done } を返すオブジェクト。for-of・スプレッド・分割代入は Symbol.iterator でこのイテレータを取り出し、done が true になるまで next() を回す。」が本当に評価軸になるか確認する。
  • 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
  • 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
  • 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
  • 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。

次に確認する観点

JavaScriptジェネレータイテレータ非同期ブラウザJavaScriptジェネレータイテレータ
参考: 公式情報