JavaScriptの基礎 - イテレータとジェネレータ

提供: MochiuWiki : SUSE, EC, PCB

概要

イテレータ (Iterator) は、シーケンスを定義するための統一的なインターフェースである。
next() メソッドを持つオブジェクトであり、呼び出すたびに { value, done } 形式のオブジェクトを返す。

イテラブル (Iterable) は Symbol.iterator メソッドを持つオブジェクトであり、このメソッドがイテレータを返す。
配列、文字列、Map、Set 等の組み込みオブジェクトはイテラブルとして実装されており、for...of 文やスプレッド構文等で利用できる。

ジェネレータ関数 (function*) は、イテレータを簡潔に作成するための特殊な関数である。
yield キーワードで実行を一時停止して値を返し、次の呼び出しで再開するという動作を実現する。

ジェネレータが返すオブジェクトはイテレータかつイテラブルであるため、for...of 文等でそのまま利用できる。

ES2025では Iterator Helpers が標準化され、Iterator.prototypemapfiltertake 等のメソッドが追加された。

これらのメソッドは遅延評価 (Lazy Evaluation) によって動作するため、中間配列を生成せずにメモリ効率の良いパイプライン処理を記述できる。


イテレータプロトコル

JavaScriptのイテレータは、イテレータプロトコルとイテラブルプロトコルの2つのプロトコルで定義される。
これらのプロトコルを実装することで、独自のオブジェクトを for...of やスプレッド構文と組み合わせて使用できるようになる。

イテレータとは

イテレータは next() メソッドを持つオブジェクトである。
next() を呼び出すと、以下の2つのプロパティを持つオブジェクトを返す。

  • value
    現在の要素の値。
    code>done が true の場合は、一般的に undefined

  • done
    反復が完了したかどうかを示す真偽値
    完了していれば true、続きがあれば false


カスタムイテレータの作成例を以下に示す。

 // カスタムイテレータの作成
 function createRangeIterator(start, end) {
    let current = start;
    return {
       next() {
          if (current <= end) {
             return { value: current++, done: false };
          }
          return { value: undefined, done: true };
       }
    };
 }
 
 const iter = createRangeIterator(1, 3);
 console.log(iter.next());  // { value: 1, done: false }
 console.log(iter.next());  // { value: 2, done: false }
 console.log(iter.next());  // { value: 3, done: false }
 console.log(iter.next());  // { value: undefined, done: true }


イテラブルプロトコル

イテラブルは Symbol.iterator メソッドを持つオブジェクトである。

Symbol.iterator を呼び出すとイテレータオブジェクトを返す。

カスタムイテラブルの作成例を以下に示す。

 // カスタムイテラブルの作成
 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...ofで使用できる
 for (const num of range) {
    console.log(num);  // 1, 2, 3, 4, 5
 }
 
 // スプレッド構文でも使用できる
 console.log([...range]);  // [1, 2, 3, 4, 5]


組み込みイテラブル

JavaScriptには、最初からイテラブルプロトコルを実装している組み込みオブジェクトが多数存在する。

組み込みイテラブル一覧
イテレートされる値 備考
Array 各要素の値 最も基本的なイテラブル
String 各文字 (Unicode コードポイント単位) サロゲートペアを正しく扱う
Map [key, value] のペア 挿入順で反復
Set 各要素の値 挿入順で反復、重複なし
TypedArray 各要素の値 Int32Array 等の型付き配列
arguments 各引数の値 関数内の暗黙の引数オブジェクト
NodeList 各DOMノード querySelectorAll の戻り値等
ジェネレータオブジェクト yield した値 function* の戻り値



for...of とイテレータ

for...of 文はイテラブルプロトコルに基づいて動作する反復構文である。

for...of の動作原理

for...of は内部的に以下の手順で動作する。

  1. 対象オブジェクトの Symbol.iterator メソッドを呼び出してイテレータを取得する。
  2. イテレータの next() メソッドを繰り返し呼び出す。
  3. next() の戻り値の donetrue になるまで、value を変数に代入してループ本体を実行する。
  4. donetrue になるとループを終了する。


 // for...of の基本的な使い方
 const arr = [10, 20, 30];
 for (const value of arr) {
    console.log(value);  // 10, 20, 30
 }
 
 // 内部動作を明示的に再現した例
 const iterator = arr[Symbol.iterator]();
 let result = iterator.next();
 while (!result.done) {
    console.log(result.value);  // 10, 20, 30
    result = iterator.next();
 }


for...of が使用できる場面

イテラブルを消費できる構文・関数を以下に示す。

イテラブルを消費する主な構文・API
構文・API 説明
for...of 最も基本的なイテレータ消費構文
スプレッド構文 (...) [...iterable] のように配列に展開する。
分割代入 const [a, b] = iterable のように変数に展開する。
Array.from() イテラブルを配列に変換する。
Map / Set コンストラクタ new Map(iterable) のようにコレクションを初期化する。
Promise.all() / Promise.race() イテラブルのPromiseをまとめて処理する。
yield* ジェネレータ内で別のイテラブルに処理を委譲する。


for...of と for...in の違い

for...offor...in は見た目が似ているが、用途が大きく異なる。

for...of と for...in の比較
項目 for...of for...in
対象 イテラブルオブジェクト すべてのオブジェクト
返す値 要素の値 プロパティ名 (文字列)
プロトタイプチェーン 辿らない 列挙可能なプロパティを辿る
配列での挙動 要素の値を返す インデックスを文字列で返す
主な用途 イテラブルの値を処理する オブジェクトのプロパティを列挙する


 const arr = ["a", "b", "c"];
 
 // for...of : 要素の値を反復
 for (const value of arr) {
    console.log(value);  // "a", "b", "c"
 }
 
 // for...in : インデックスを文字列で反復
 for (const key in arr) {
    console.log(key);    // "0", "1", "2" (文字列)
 }
 
 // for...inはプロトタイプチェーンも辿る (注意)
 const obj = { a: 1, b: 2 };
 Object.prototype.extra = 99;  // プロトタイプにプロパティを追加
 for (const key in obj) {
    console.log(key);  // "a", "b", "extra" (意図しない結果)
 }
 // hasOwnPropertyでフィルタリングする
 for (const key in obj) {
    if (Object.hasOwn(obj, key)) {
       console.log(key);  // "a", "b"
    }
 }
 delete Object.prototype.extra;



スプレッド構文と分割代入との関係

スプレッド構文と分割代入は、どちらもイテラブルプロトコルに依存している。

配列に限らず、Symbol.iterator を実装したあらゆるオブジェクトに対してこれらの構文を使用できる。

 // カスタムイテラブルに対するスプレッド構文
 const range = {
    [Symbol.iterator]() {
       let i = 1;
       return { next: () => i <= 3 ? { value: i++, done: false } : { done: true } };
    }
 };
 
 console.log([...range]);           // [1, 2, 3]
 const [first, second] = range;
 console.log(first, second);        // 1 2
 
 // Mapのスプレッド (エントリのペアが展開される)
 const map = new Map([["a", 1], ["b", 2]]);
 console.log([...map]);             // [["a", 1], ["b", 2]]
 
 const [[k1, v1], [k2, v2]] = map;
 console.log(k1, v1, k2, v2);       // "a" 1 "b" 2



ジェネレータ関数

ジェネレータ関数は、イテレータを簡潔に定義するための特殊な関数である。
function* 構文で宣言し、yield キーワードで値を返しながら実行を一時停止する。

基本構文

ジェネレータ関数を呼び出すと、関数本体はすぐに実行されず、ジェネレータオブジェクトが返される。
ジェネレータオブジェクトはイテレータかつイテラブルである。

 // ジェネレータ関数の宣言
 function* simpleGenerator() {
    yield 1;
    yield 2;
    yield 3;
 }
 
 const gen = simpleGenerator();  // ジェネレータオブジェクトを取得
 
 // イテレータとして使用
 console.log(gen.next());        // { value: 1, done: false }
 console.log(gen.next());        // { value: 2, done: false }
 console.log(gen.next());        // { value: 3, done: false }
 console.log(gen.next());        // { value: undefined, done: true }
 
 // イテラブルとして使用 (for...of, スプレッド構文等)
 for (const value of simpleGenerator()) {
    console.log(value);          // 1, 2, 3
 }
 
 console.log([...simpleGenerator()]);  // [1, 2, 3]


yieldの動作

yield は関数の実行を一時停止して、指定した値を呼び出し元に返す。
次に、next() が呼び出されると、yield の直後から実行が再開される。

 function* stepGenerator() {
    console.log("ステップ1開始");
    yield "step1";
 
    console.log("ステップ2開始");
    yield "step2";
 
    console.log("完了");
 }
 
 const gen = stepGenerator();
 
 console.log(gen.next());
 // "ステップ1開始" が出力される
 // { value: "step1", done: false } が返る
 
 console.log(gen.next());
 // "ステップ2開始" が出力される
 // { value: "step2", done: false } が返る
 
 console.log(gen.next());
 // "完了" が出力される
 // { value: undefined, done: true } が返る


next() による値の送信

next(value) の引数として値を渡すことで、ジェネレータ内の yield 式の評価結果として値を受け取ることができる。

最初の next() 呼び出しに渡した値は無視される。
これは、最初の next() が実行を開始するまでの間、対応する yield 式がまだ存在しないためである。

 function* accumulator() {
    let total = 0;
    while (true) {
       // yield 式の値が next() の引数として渡される
       const input = yield total;
       if (input === null) break;
       total += input;
    }
    return total;
 }
 
 const gen = accumulator();
 console.log(gen.next());      // { value: 0, done: false } (最初の呼び出し、引数は無視)
 console.log(gen.next(10));    // { value: 10, done: false }
 console.log(gen.next(20));    // { value: 30, done: false }
 console.log(gen.next(5));     // { value: 35, done: false }
 console.log(gen.next(null));  // { value: 35, done: true } (break でジェネレータ終了)


return と throw

ジェネレータオブジェクトには、next() の他に return()throw() メソッドがある。

各メソッドの動作
メソッド 動作
return(value) ジェネレータを強制的に終了する。
{ value: value, done: true } を返す。
throw(error) ジェネレータ内で例外を発生させる。
try...catch で捕捉できる。


 function* withCleanup() {
    try {
       yield 1;
       yield 2;
       yield 3;
    }
    finally {
       // return() や throw() が呼ばれてもここは必ず実行される
       console.log("クリーンアップ処理");
    }
 }
 
 // return() の使用例
 const gen1 = withCleanup();
 console.log(gen1.next());         // { value: 1, done: false }
 console.log(gen1.return("終了")); // "クリーンアップ処理" が出力される
                                   // { value: "終了", done: true }
 
 // throw() の使用例
 function* withErrorHandling() {
    try {
       yield 1;
       yield 2;
    }
    catch (e) {
       console.log("エラーをキャッチ:", e.message);
       yield "エラー後の値";
    }
 }
 
 const gen2 = withErrorHandling();
 console.log(gen2.next());                         // { value: 1, done: false }
 console.log(gen2.throw(new Error("テストエラー")));
 // "エラーをキャッチ: テストエラー" が出力される
 // { value: "エラー後の値", done: false }


yield* (委譲)

yield* は別のイテラブルまたはジェネレータに処理を委譲する構文である。

委譲されたイテラブルの全要素を yield した後、yield* 式自体の評価結果として、
委譲先ジェネレータのreturn値 (最後の done: truevalue) が得られる。

 function* inner() {
    yield "a";
    yield "b";
    return "innerの戻り値";  // done: true の value
 }
 
 function* outer() {
    yield 1;
    const result = yield* inner();  // inner の要素を委譲し、return 値を受け取る
    console.log("inner の戻り値:", result);  // "inner の戻り値: inner の戻り値"
    yield 2;
 }
 
 console.log([...outer()]);  // [1, "a", "b", 2]
 // "inner の戻り値: inner の戻り値"も出力される
 
 // yield* は通常のイテラブルにも使用できる
 function* flatten(arr) {
    for (const item of arr) {
       if (Array.isArray(item)) {
          yield* flatten(item);  // 再帰的に委譲
       } else {
          yield item;
       }
    }
 }
 
 console.log([...flatten([1, [2, [3, 4]], 5])]);  // [1, 2, 3, 4, 5]


サンプルコード

無限シーケンス

ジェネレータは遅延評価により無限シーケンスを表現できる。

for...ofbreaktake() 等で必要な分だけ取り出して使用する。

 // フィボナッチ数列 (無限シーケンス)
 function* fibonacci() {
    let a = 0, b = 1;
    while (true) {
       yield a;
       [a, b] = [b, a + b];
    }
 }
 
 // 先頭10個を取り出す
 const fib = fibonacci();
 const first10 = [];
 for (let i = 0; i < 10; i++) {
    first10.push(fib.next().value);
 }
 console.log(first10);  // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
 
 // IDジェネレータ
 function* idGenerator(prefix = "id") {
    let id = 1;
    while (true) {
       yield `${prefix}-${id++}`;
    }
 }
 
 const gen = idGenerator("user");
 console.log(gen.next().value);   // "user-1"
 console.log(gen.next().value);   // "user-2"
 console.log(gen.next().value);   // "user-3"


遅延評価

ジェネレータは必要になるまで次の値を計算しない遅延評価により、大量データをメモリ効率良く処理できる。

 // 大量のデータを逐次処理する例
 function* readLines(text) {
    const lines = text.split("\n");
    for (const line of lines) {
       yield line.trim();
    }
 }
 
 function* filterNonEmpty(iterable) {
    for (const item of iterable) {
       if (item.length > 0) yield item;
    }
 }
 
 function* parseCSV(lines) {
    for (const line of lines) {
       yield line.split(",").map(s => s.trim());
    }
 }
 
 const csvData = `
 Alice, 30, Tokyo
 Bob, 25, Osaka
 
 Charlie, 35, Kyoto
 `.trim();
 
 // パイプライン : 各段階で必要な分だけ処理する
 const pipeline = parseCSV(filterNonEmpty(readLines(csvData)));
 for (const row of pipeline) {
    console.log(row);  // ["Alice", "30", "Tokyo"], ["Bob", "25", "Osaka"], ...
 }



イテレータヘルパー (ES2025)

ES2025でイテレータヘルパーが標準化され、Iterator.prototype に配列の反復メソッドに相当するメソッド群が追加された。

概要

イテレータヘルパーは Iterator.prototype に追加されたメソッド群であり、全ての最新のWebブラウザ (Chrome 122+、Firefox 131+、Safari 18.2+) で利用できる。

配列の map()filter() と異なり、イテレータヘルパーは遅延評価で動作する。
メソッドチェーンを組んでも中間配列を生成しないため、大量データや無限シーケンスを効率的に処理できる。

map

Iterator.prototype.map(fn) は各要素にマッピング関数を適用した新しいイテレータを返す。

 function* naturals() {
    let i = 1;
    while (true) yield i++;
 }
 
 // 各要素を2倍にするイテレータ (遅延評価)
 const doubled = naturals().map(n => n * 2);
 console.log(doubled.next());  // { value: 2, done: false }
 console.log(doubled.next());  // { value: 4, done: false }
 console.log(doubled.next());  // { value: 6, done: false }


filter

Iterator.prototype.filter(fn) はフィルタ関数が true を返す要素のみを含む新しいイテレータを返す。

 function* naturals() { let i = 1; while (true) yield i++; }
 
 // 偶数のみを通すイテレータ (遅延評価)
 const evens = naturals().filter(n => n % 2 === 0);
 console.log(evens.next());  // { value: 2, done: false }
 console.log(evens.next());  // { value: 4, done: false }


take

Iterator.prototype.take(n) は、先頭 n 個の要素を返した後に完了するイテレータを返す。

 function* naturals() { let i = 1; while (true) yield i++; }
 
 // 先頭5個だけを取り出す
 console.log([...naturals().take(5)]);  // [1, 2, 3, 4, 5]


drop

Iterator.prototype.drop(n) は、先頭 n 個をスキップした残りの要素を返すイテレータを返す。

 function* naturals() { let i = 1; while (true) yield i++; }
 
 // 先頭5個をスキップして次の3個を取得
 console.log([...naturals().drop(5).take(3)]);  // [6, 7, 8]


reduce

Iterator.prototype.reduce(fn, initial) は、全ての要素を集約した値を返す即時評価メソッドである。

 // 1から10の合計
 function* range(start, end) {
    for (let i = start; i <= end; i++) yield i;
 }
 
 const sum = range(1, 10).reduce((acc, n) => acc + n, 0);
 console.log(sum);  // 55


toArray

Iterator.prototype.toArray() は、イテレータの全ての要素を配列に変換する即時評価メソッドである。

 function* range(start, end) {
    for (let i = start; i <= end; i++) yield i;
 }
 
 const arr = range(1, 5).toArray();
 console.log(arr);  // [1, 2, 3, 4, 5]


メソッドチェーン

イテレータヘルパーのメソッドを連鎖させることで、宣言的で読みやすいパイプライン処理を記述できる。

遅延評価により、toArray()reduce() 等の即時評価メソッドが呼ばれるまで要素は消費されない。

 function* naturals() { let i = 1; while (true) yield i++; }
 
 // 自然数の中から偶数を選び、それぞれ2乗した先頭5個を配列で取得
 const result = naturals()
    .filter(n => n % 2 === 0)   // 偶数のみ (遅延)
    .map(n => n * n)             // 2乗 (遅延)
    .take(5)                     // 先頭5個 (遅延)
    .toArray();                  // ここで初めて評価が実行される
 
 console.log(result);  // [4, 16, 36, 64, 100]
 
 // 配列の反復メソッドとの比較
 // Array.prototype を使う場合、各ステップで中間配列が生成される
 const arrayResult = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    .filter(n => n % 2 === 0)   // 中間配列: [2, 4, 6, 8, 10]
    .map(n => n * n)             // 中間配列: [4, 16, 36, 64, 100]
    .slice(0, 5);                // [4, 16, 36, 64, 100]


下表に、イテレータヘルパーの全メソッドを示す。

イテレータヘルパー一覧
メソッド 戻り値 評価タイミング 説明
map(fn) Iterator 遅延 各要素に関数を適用する
filter(fn) Iterator 遅延 条件に合う要素のみを通す
take(n) Iterator 遅延 先頭 n 個で完了するイテレータを返す
drop(n) Iterator 遅延 先頭 n 個をスキップする
flatMap(fn) Iterator 遅延 マップして1段階フラットにする
reduce(fn, init) 即時 すべての要素を集約して値を返す
toArray() Array 即時 すべての要素を配列に変換する
forEach(fn) undefined 即時 各要素に対して副作用を実行する
some(fn) boolean 即時 いずれかの要素が条件を満たすか
every(fn) boolean 即時 すべての要素が条件を満たすか
find(fn) 値 / undefined 即時 最初に条件を満たす要素を返す


Iterator.from

Iterator.from(obj) は既存のイテレータまたはイテラブルから、イテレータヘルパーのメソッドを持つ Iterator オブジェクトを作成する静的メソッドである。
これにより、組み込みの配列やカスタムイテラブルに対してもイテレータヘルパーを適用できる。

 // 配列からイテレータヘルパーを使用する
 const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
 const result = Iterator.from(arr)
    .filter(n => n % 3 === 0)
    .map(n => n * 10)
    .toArray();
 console.log(result);  // [30, 60, 90]
 
 // カスタムイテラブルからイテレータヘルパーを使用する
 const range = {
    [Symbol.iterator]() {
       let i = 1;
       return { next: () => i <= 5 ? { value: i++, done: false } : { done: true } };
    }
 };
 
 const rangeResult = Iterator.from(range)
    .map(n => n * n)
    .toArray();
 console.log(rangeResult);  // [1, 4, 9, 16, 25]
 
 // NodeList (DOM) にも適用できる
 // const items = Iterator.from(document.querySelectorAll("li"))
 //    .filter(el => el.classList.contains("active"))
 //    .map(el => el.textContent)
 //    .toArray();



関連情報