JavaScript ジェネレータのユースケース
JavaScript ジェネレータ(function*)は ES2015 から言語仕様の一部となっていますが、多くのフロントエンド開発者は、ジェネレータの方が適している場面でも配列や Promise チェーンを使用しています。ジェネレータの本質的な価値は処理速度ではなく、遅延評価、合成可能性、そしてイテレーションの正確な制御にあります。本記事では、ジェネレータが現代のフロントエンドコードで真価を発揮する場面を解説します。
重要なポイント
- ジェネレータは遅延評価によってオンデマンドで値を生成し、不要な計算と中間配列を回避します
- Iterator Helpers API により、ジェネレータが返すイテレータに組み込みの
map、filter、takeメソッドが提供され、カスタムユーティリティ関数が不要になります 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:組み込みの遅延パイプライン
カスタムの map、filter、take ユーティリティを書くことは、かつては必要な定型コードでした。Iterator Helpers API — 現在すべてのモダンブラウザで利用可能 — は、これらを同期イテレータに直接追加します:
const result = naturals()
.filter(n => n % 2 === 0)
.map(n => n * n)
.take(5)
.toArray() // [0, 4, 16, 36, 64]
各ステップは遅延的です。.toArray() が評価をトリガーします。これにより、サードパーティライブラリなしでジェネレータベースのパイプラインが大幅にクリーンになります。これらのヘルパーは同期イテレータに適用されることに注意してください — 非同期イテレータヘルパーはまだ環境間で標準化されていません。
Discover how at OpenReplay.com.
ツリーとグラフの走査
ジェネレータは再帰的な構造の走査に自然にフィットします。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.