Back

JavaScript ジェネレータのユースケース

JavaScript ジェネレータのユースケース

JavaScript ジェネレータ(function*)は ES2015 から言語仕様の一部となっていますが、多くのフロントエンド開発者は、ジェネレータの方が適している場面でも配列や Promise チェーンを使用しています。ジェネレータの本質的な価値は処理速度ではなく、遅延評価、合成可能性、そしてイテレーションの正確な制御にあります。本記事では、ジェネレータが現代のフロントエンドコードで真価を発揮する場面を解説します。

重要なポイント

  • ジェネレータは遅延評価によってオンデマンドで値を生成し、不要な計算と中間配列を回避します
  • Iterator Helpers API により、ジェネレータが返すイテレータに組み込みの mapfiltertake メソッドが提供され、カスタムユーティリティ関数が不要になります
  • yield* により、ツリーやグラフの再帰的な走査が読みやすく、かつ遅延的に実行できます
  • 非同期ジェネレータ(async function*)は for await...of と組み合わせることで、ページネーションやバッチ処理されたデータ取得を最小限の状態管理で処理できます

JavaScript ジェネレータの特徴

ジェネレータ関数はイテレータを返します。関数を呼び出してもコードは実行されず、.next() メソッドを持つオブジェクトが返されます。.next() を呼び出すたびに、関数本体が次の yield まで実行され、その後一時停止し、呼び出し間でローカル状態が保持されます。

function* range(start, end) {
  for (let i = start; i < end; i++) yield i
}

for (const n of range(0, 5)) {
  console.log(n) // 0, 1, 2, 3, 4
}

ジェネレータはイテレータプロトコルを実装しているため、for...of、スプレッド構文、分割代入と直接連携でき、アダプタは不要です。

JavaScript における遅延イテレーション:データを実体化せずに処理する

配列ではなくジェネレータを使用する主な理由は遅延イテレーションです。値は要求されたときにのみ生成されます。これが重要になるのは次のような場合です:

  • データセット全体が大きく、その一部だけが必要な場合
  • 各値の計算コストが高い場合
  • シーケンスが概念的に無限である場合
function* naturals() {
  let n = 0
  while (true) yield n++
}

// break ポイントまでの値のみを計算
for (const n of naturals()) {
  if (n > 100) break
}

中間配列は作成されません。break ポイントを超える値は計算されません。

Iterator Helpers API:組み込みの遅延パイプライン

カスタムの mapfiltertake ユーティリティを書くことは、かつては必要な定型コードでした。Iterator Helpers API — 現在すべてのモダンブラウザで利用可能 — は、これらを同期イテレータに直接追加します:

const result = naturals()
  .filter(n => n % 2 === 0)
  .map(n => n * n)
  .take(5)
  .toArray() // [0, 4, 16, 36, 64]

各ステップは遅延的です。.toArray() が評価をトリガーします。これにより、サードパーティライブラリなしでジェネレータベースのパイプラインが大幅にクリーンになります。これらのヘルパーは同期イテレータに適用されることに注意してください — 非同期イテレータヘルパーはまだ環境間で標準化されていません。

ツリーとグラフの走査

ジェネレータは再帰的な構造の走査に自然にフィットします。DOM ライクなツリーの深さ優先走査は次のように簡潔になります:

function* walkTree(node) {
  yield node
  for (const child of node.children ?? []) {
    yield* walkTree(child)
  }
}

for (const node of walkTree(rootNode)) {
  if (node.type === 'input') processInput(node)
}

yield* はネストされたジェネレータに処理を委譲し、再帰を読みやすく保ち、走査を遅延的に実行します — 必要なものを見つけたらすぐに停止できます。

JavaScript における非同期ジェネレータ:ページネーションとバッチデータ取得

async function* はこのパターンを非同期シーケンスに拡張します。for await...of と組み合わせることで、ページネーションされた API レスポンスに適しています:

async function* fetchPages(url) {
  let nextUrl = url
  while (nextUrl) {
    const res = await fetch(nextUrl)
    const data = await res.json()
    yield data.items
    nextUrl = data.nextPage ?? null
  }
}

for await (const batch of fetchPages('/api/records')) {
  processBatch(batch)
}

各ページはループが進むときにのみ取得されます。すべてのページを事前に収集したり、ページネーション状態を外部で管理したりする必要はありません — ジェネレータがそれを保持します。

ジェネレータを使用すべきでない場合

ジェネレータは間接性を追加します。完全に消費する単純な配列変換の場合、チェーンされた配列メソッドの方が明確です。シーケンスが大きい、潜在的に無限である、または無駄な計算なしに早期停止する必要がある場合にジェネレータを使用してください。

まとめ

JavaScript ジェネレータは 3 つの領域で輝きます:大規模または無限のシーケンスに対する遅延イテレーション、合成可能なデータパイプライン(特に Iterator Helpers API を使用する場合)、そして順次的で状態を持つ制御が必要な非同期データ取得です。ジェネレータは配列や async/await の置き換えではありません — 一度にすべてではなく、オンデマンドで値を生成する必要がある場合に適したツールです。

よくある質問

はい。ジェネレータは React コンポーネントが消費するデータのシーケンスを生成するのに適しています。useEffect や useMemo フック内でジェネレータを呼び出して、遅延的に値を計算できます。ただし、ジェネレータをコンポーネント関数自体として使用しないでください — React はコンポーネントがイテレータではなく JSX を返すことを期待しています。

ジェネレータは最後の yield ポイントで一時停止したままになります。イテレータオブジェクトへの参照が残っていない場合、ガベージコレクションの対象になります。イテレーションが早期に停止したときにクリーンアップロジックを実行する必要がある場合は、yield を try-finally ブロックでラップしてください。finally ブロックは、イテレータの return メソッドが呼び出されたとき、またはジェネレータがガベージコレクションされたときに実行されます。

完全に消費される小さなコレクションの場合、ジェネレータは一時停止と再開のメカニズムによるわずかなオーバーヘッドがあります。ほとんどのアプリケーションでは、パフォーマンスの差は無視できる程度です。大規模なデータセットを部分的に処理する場合、ジェネレータは中間配列の割り当てを回避し、要求されない値の計算をスキップするため、実際にはより高速になります。

非同期ジェネレータは値が利用可能になると段階的に yield しますが、Promise ベースのアプローチはすべてのデータが収集されるまで待ってから返します。つまり、非同期ジェネレータを使用すると、最初のバッチの結果をすぐに処理し始めることができ、ピークメモリ使用量を削減し、後続の各フェッチがいつ発生するかをより細かく制御できます。

Complete picture for complete understanding

Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Check our GitHub repo and join the thousands of developers in our community.

OpenReplay