Iterator パターン

コレクションの内部構造を公開せずに、要素を順番にアクセスする手段を提供するデザインパターン

設計パターンJavaScript

Iterator パターンとは

Iterator パターンは、コレクション (配列、ツリー、グラフ) の内部構造を公開せずに、要素を 1 つずつ順番にアクセスする統一的なインターフェースを提供するデザインパターンである。GoF の 23 パターンの 1 つで、現代の言語では言語仕様に組み込まれている。

配列は 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 は無限シーケンスを表現できる。全要素をメモリに載せる必要がなく、必要な分だけ生成する遅延評価が自然に実現される。

Async Iterator

for await...ofSymbol.asyncIterator で、非同期データソースを反復できる。DynamoDB のページネーションや、ストリーミング API の処理に使う。

async function* paginateQuery(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 paginateQuery({ TableName: 'Orders', /* ... */ })) {
  console.log(item);
}

AWS SDK v3 の paginateQuery もこのパターンで実装されている。呼び出し側はページネーションの存在を意識せず、for await...of で全アイテムを処理できる。

遅延評価のメリット

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));
}

Iterator パターンの適用場面

場面 具体例
ページネーション DynamoDB の Query、REST API のページ送り
ファイル処理 大きなファイルを 1 行ずつ読む
ストリーミング WebSocket メッセージ、SSE
ツリー走査 DOM ツリー、ファイルシステムの再帰探索
無限シーケンス フィボナッチ数列、乱数生成

全体像を把握するには関連書籍も有用。

関連用語