【JavaScript】イテレーターとジェネレーターを完全理解!データストリーム制御と非同期処理への応用
※本ページのリンクにはプロモーションが含まれています。
こんにちは、Ryohei(@ityryohei)です!
モダンJavaScriptにおいて、配列や文字列といったデータの集合を扱う際に、その要素を一つずつ取り出す「反復処理」の仕組みを理解することは非常に重要です。
この反復処理の内部動作を司るのがイテレーター(Iterator)、そしてそのイテレーターを簡単に作成するための強力な構文がジェネレーター(Generator)です。これらは、従来のforループやforEachでは難しい大規模データの効率的な処理や、async/awaitに依存しない低レベルな非同期処理の制御を可能にします。
本記事では、イテレーターとジェネレーターの基本的な原理から、ジェネレーター関数(function*)を使った処理の一時停止と再開のメカニズム、そして実務での応用例までを徹底解説します。
for...ofループはよく使うけど、これって裏側でどう動いているんだろう?それに、function* っていう見慣れない構文はいったい何のためにあるんだろう?
上記の疑問にお答えします。
では、解説していきます。なぜジェネレーターが必要なのか
従来の反復処理の限界
配列や文字列、Map、Setなどのデータ構造はイテラブル(Iterable)と呼ばれ、for...ofループで要素を順番に取り出せます。
JavaScript
const numbers = [1, 2, 3];
// for...ofループは、イテラブルなオブジェクトから要素を順番に取り出す
for (const num of numbers) {
console.log(num); // 1, 2, 3
}
しかし、for...ofがどのように動いているのか、そして、要素を無限に生成し続けるようなデータストリームをどう扱えばいいのか、従来の構文では対処が困難でした。
イテレーター(Iterator)の原理
すべてのイテラブルなオブジェクトは、内部でイテレーターと呼ばれるオブジェクトを持っています。イテレーターは、以下のルールを持つオブジェクトです。
Symbol.iteratorという特殊なプロパティを持つ(イテラブルの条件)。next()メソッドを持つ。next()メソッドは、要素の値(value)と処理が完了したか(done)を持つオブジェクト{ value: any, done: boolean }を返す。
for...ofループは、このnext()メソッドを要素がなくなるまで自動で呼び出し続けるためのシンタックスシュガー(糖衣構文)に過ぎません。
ジェネレーター(Generator)の役割
ジェネレーターは、この複雑なイテレーターオブジェクトを、シンプルな関数構文で自動的に生成してくれる仕組みです。
- function*:
ジェネレーター関数を定義する構文です。 - yield:
ジェネレーター関数の実行を一時停止し、値を外側に返すためのキーワードです。次にnext()が呼ばれるまで、関数は中断された状態を保ちます。
これにより、「必要な時だけ」データを生成・提供する、遅延評価(Lazy Evaluation)が実現できます。
基本的な使い方:ジェネレーター関数の定義とyieldの動作
ステップ1: ジェネレーター関数の定義
関数名の前にアスタリスク(*)を付けてジェネレーター関数を定義します。この関数を実行しても、すぐには中のコードは実行されず、ジェネレーターオブジェクト(イテレーター)が返されます。
JavaScript
function* numberGenerator() {
yield 1; // 実行を一時停止
yield 2; // 再開後、実行を一時停止
return 3; // 最後の値として返し、終了
}
const generator = numberGenerator(); // ジェネレーターオブジェクトを取得
ステップ2: next() メソッドによる実行の制御
ジェネレーターオブジェクトのnext()メソッドを呼び出すことで、関数は次のyieldまで実行され、値が返されます。
JavaScript
console.log(generator.next()); // -> { value: 1, done: false }
console.log(generator.next()); // -> { value: 2, done: false }
console.log(generator.next()); // -> { value: 3, done: true } (returnされた値)
console.log(generator.next()); // -> { value: undefined, done: true } (終了済み)
実行が一時停止された状態(コルーチンの状態)がメモリ上に保持され、next()が呼ばれるたびに実行が再開されるのがジェネレーターの最大の特徴です。
実践コード:ジェネレーターの応用テクニック
1. 無限シーケンスの実現(遅延評価)
ジェネレーターを使えば、要素をメモリに保持することなく、無限に要素を生成し続けることができます。
JavaScript
// 無限に連番を生成するジェネレーター
function* infiniteSequence() {
let i = 0;
while (true) {
yield i++;
}
}
const sequence = infiniteSequence();
console.log(sequence.next().value); // -> 0
console.log(sequence.next().value); // -> 1
// ... 必要に応じて何度でもnext()を呼び出せる
これにより、巨大なデータセットや無限のイベントストリームを扱う際に、メモリ効率の良い処理が可能です。
2. yieldによる値の双方向通信
ジェネレーターは値を返すだけでなく、next()の引数を通じて外部から値を受け取ることもできます。
JavaScript
function* calculator() {
const a = yield "最初の数値を入力してください"; // 実行が停止し、メッセージを返す
const b = yield "次の数値を入力してください"; // 実行が停止し、メッセージを返す
return a + b;
}
const calc = calculator();
// 1. 最初は関数実行(最初のyieldまで)
console.log(calc.next().value); // -> 最初の数値を入力してください
// 2. nextの引数 '10' が、最初の yieldの左辺 a に代入される
console.log(calc.next(10).value); // -> 次の数値を入力してください
// 3. nextの引数 '5' が、2番目の yieldの左辺 b に代入される
console.log(calc.next(5).value); // -> 15
これは、非同期処理の制御フローを同期的なコードのように書くための強力な手段であり、async/awaitが登場する以前は、Promiseベースの非同期処理を扱うための主要なパターンでした。
補足:イテレーターを自分で実装する
ジェネレーターはイテレーターを自動生成してくれますが、既存のオブジェクトをイテラブルにするために、自分でイテレーターを実装する方法を理解しておくことも重要です。
オブジェクトをイテラブルにするには、Symbol.iteratorという特別なキーを持つメソッドを定義します。
JavaScript
const team = {
members: ['Taro', 'Jiro', 'Goro'],
// このメソッドがイテレーターオブジェクトを返す(イテラブルの定義)
[Symbol.iterator]: function* () {
// ジェネレーターを使えば簡単にイテレーターを実装できる
yield* this.members; // yield* は別のイテレーター(この場合は配列)の要素を全て展開する
}
};
// これで for...of で回せるようになる
for (const member of team) {
console.log(member); // Taro, Jiro, Goro
}
最後に
イテレーターとジェネレーターは、JavaScriptにおけるデータの「流れ」を根幹から理解し、制御するための非常に強力なツールです。
- 遅延評価:
yieldを使うことで、すべてのデータを一度に生成・メモリに保持することなく、必要な時だけ処理を実行できます。 - 効率的なデータ処理:
無限シーケンスや大規模データの処理において、メモリとパフォーマンスの効率を大幅に向上させます。 - 非同期制御の基礎:
yieldによる実行の停止と再開のメカニズムは、Promiseと組み合わせることで、後のasync/awaitの基盤となる非同期制御パターンを可能にしました。
これらの概念をマスターすることで、あなたはより高度でスケーラブルなJavaScriptアプリケーションを構築できるようになるでしょう。
以上、イテレーターとジェネレーターについての解説でした!