Iterator パターン
コレクションの内部構造を公開せずに要素へ順番にアクセスする手段を提供するデザインパターン。JavaScript の Iterator プロトコル / Generator / Async Iterator と遅延評価の落とし穴を解説
Iterator パターンとは
Iterator パターンは、コレクション (配列、ツリー、グラフ) の内部構造を公開せずに、要素を 1 つずつ順番にアクセスする統一的なインターフェースを提供するデザインパターンである。GoF の 23 パターンの 1 つで、JavaScript や Python など多くの言語では言語仕様の側に組み込まれている。
配列は for ループで簡単に反復できるが、ツリー構造やページネーション付きの API レスポンスはそうはいかない。Iterator パターンは、データ構造の違いを隠蔽し、統一的な for...of で反復可能にする。
JavaScript の Iterator プロトコル
JavaScript には言語レベルで Iterator が組み込まれている。Symbol.iterator メソッドを実装したオブジェクトは Iterable と呼ばれ、for...of、スプレッド構文、分割代入で使える。
class Range {
constructor(private start: number, private end: number) {}
[Symbol.iterator]() {
let current = this.start;
const end = this.end;
return {
next(): IteratorResult<number> {
if (current <= end) return { value: current++, done: false };
return { value: undefined, done: true };
},
};
}
}
for (const n of new Range(1, 5)) console.log(n); // 1, 2, 3, 4, 5
console.log([...new Range(1, 3)]); // [1, 2, 3]
const [first, second] = new Range(10, 20); // 10, 11
next() メソッドが { value, done } を返すのが Iterator プロトコルの核心だ。done: true になるまで next() を呼び続ける。
Generator 関数
Generator 関数 (function*) は Iterator を簡潔に作成する構文糖だ。yield で値を 1 つずつ返し、関数の実行が一時停止・再開される。
function* fibonacci(): Generator<number> {
let [a, b] = [0, 1];
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
// 無限シーケンスから最初の 10 個だけ取得
function take<T>(n: number, iter: Iterable<T>): T[] {
const result: T[] = [];
for (const value of iter) {
result.push(value);
if (result.length >= n) break;
}
return result;
}
take(10, fibonacci()); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
Generator は無限シーケンスを表現できる。全要素をメモリに載せる必要がなく、必要な分だけ生成する遅延評価が自然に実現される。
ここで注意したいのは take が break でループを抜ける点だ。for...of が早期離脱すると反復子の return() が呼ばれ、Generator はその時点で終了扱いになる。同じ Generator オブジェクトを再び for...of に渡しても、もう値は出てこない。一方 Range のように [Symbol.iterator]() が毎回新しい反復子を返すオブジェクトなら、何度でも先頭から反復できる。引数に取るのが「反復できる対象」なのか「反復子そのもの」なのかは、API を設計する側が意識して決めるべき分かれ目である。
take のような補助関数は、標準の反復子ヘルパー (Iterator.prototype.take や map、filter) で置き換えられる。2025 年 3 月以降は主要ブラウザの最新版で利用できるようになり、fibonacci().take(10).toArray() と書ける環境が増えた。古い実行環境も対象に含めるなら、上のような自前実装か polyfill が引き続き必要になる。
Async Iterator
for await...of と Symbol.asyncIterator で、非同期データソースを反復できる。DynamoDB のページネーションや、ストリーミング API の処理に使う。
async function* iterateQueryItems(params: QueryCommandInput) {
let lastKey: Record<string, any> | undefined;
do {
const result = await ddb.send(new QueryCommand({
...params,
ExclusiveStartKey: lastKey,
}));
yield* result.Items ?? [];
lastKey = result.LastEvaluatedKey;
} while (lastKey);
}
// 全ページを透過的に反復
for await (const item of iterateQueryItems({ TableName: 'Orders', /* ... */ })) {
console.log(item);
}
AWS SDK for JavaScript v3 も、同じ考え方の paginateQuery を標準で提供している。ただし 2026 年 8 月時点の実装が返すのは AsyncGenerator で、yield されるのは 1 件ずつのアイテムではなくクエリの応答そのもの、つまりページ単位である。継続キー (ExclusiveStartKey と LastEvaluatedKey) の受け渡しは SDK が引き受けてくれるが、アイテム単位で受け取りたければ上の例のように yield* result.Items ?? [] を挟む薄いラッパーを自分で書く。ここを取り違えると、ページ 1 個をアイテム 1 件と勘違いした処理を書いてしまう。
遅延評価のメリット
Iterator の最大の利点は遅延評価だ。100 万件のデータを処理する場合、配列なら全件をメモリに載せる必要があるが、Iterator なら 1 件ずつ処理できる。
// ❌ 全件をメモリに載せる
const allItems = await fetchAllItems(); // 100 万件 → メモリ不足
allItems.filter(item => item.active).map(item => transform(item));
// ✅ Iterator で 1 件ずつ処理
for await (const item of fetchItemsIterator()) {
if (item.active) await processItem(transform(item));
}
ただし 1 件ずつが常に速いわけではない。上の例で await processItem(...) を毎件待つと I/O が直列に並び、まとめて並列に投げる場合よりスループットは落ちる。メモリを守るか時間を縮めるかの取引なので、実務では数十件ずつ束にして並列度に上限を付けながら流す形が落ち着きやすい。もう 1 つの落とし穴は無限シーケンスの扱いで、fibonacci() にスプレッド構文や Array.from を使うと終わりが来ないままメモリを食い潰す。打ち切る手段と必ず組み合わせる。
Iterator パターンの適用場面
向いているのは、全体をあらかじめ手元に持てない、あるいは持ちたくないデータを扱う場面だ。代表例は次の 5 つになる。
| 場面 | 具体例 |
|---|---|
| ページネーション | DynamoDB の Query、REST API のページ送り |
| ファイル処理 | 大きなファイルを 1 行ずつ読む |
| ストリーミング | WebSocket メッセージ、SSE |
| ツリー走査 | DOM ツリー、ファイルシステムの再帰探索 |
| 無限シーケンス | フィボナッチ数列、乱数生成 |
全体像を把握するには関連書籍も有用。
この記事は役に立ちましたか?