【実務で使える】JavaScriptジェネレータ関数入門|配列生成・ストリーム処理の最適解
JavaScript の function*
と yield
が生み出すジェネレータ関数は、一見するとマニアックです。しかし配列の動的生成やストリーミング処理など、実務でありがちな“かゆい所”を解消してくれる便利ツールでもあります。
この記事では「JavaScript × ジェネレータ関数 × 実務」を軸に、仕組みから応用パターン、落とし穴まで掘り下げます。
ジェネレータ関数の基本としくみ──Generator オブジェクトと next()
ジェネレータ関数を呼び出すと即時実行はされず、Generator オブジェクト(Iterator を実装したオブジェクト)が返ります。
戻り値の next()
は { value, done }
形式のレコードを返し、done
が true
になった時点で反復は終了です。
さらに next(value)
で外部から値を送り返せるため、双方向通信が可能になります。たとえば生成途中で設定値を上書きしたいときに重宝します。
参考:MDN Web Docs
return と例外処理──安全なクリーンアップ
ジェネレータ内部で return
を呼ぶと処理が即座に完了します。リソースを確実に解放したい場合は try...finally
と組み合わせると安全です。
function* example() {
try {
yield '処理中';
} finally {
console.log('クリーンアップ実行');
}
}
const gen = example();
gen.next(); // => { value: '処理中', done: false }
gen.return(); // finally 節が実行される
実務ユースケース1:条件分岐で膨張する配列生成をスマートに
メニューやフィルターの要素をユーザー属性で切り替えると、push()
などの副作用関数が増えて可読性が低下します。ジェネレータを介せば「yield = 配列に 1 要素追加」という意図が明確になり、テストも簡単です。
従来の実装(スプレッド構文)
function getMenu(user) {
return [
{ name: 'Home', url: '/' },
{ name: 'Profile', url: '/profile' },
...(user.isAdmin ? [{ name: 'Admin', url: '/admin' }] : []),
...(user.isVIP ? [{ name: 'VIP', url: '/vip' }] : [])
];
}
ジェネレータで書き換えた実装
function getMenu(user) {
return Array.from(function* () {
yield { name: 'Home', url: '/' };
yield { name: 'Profile', url: '/profile' };
if (user.isAdmin) yield { name: 'Admin', url: '/admin' };
if (user.isVIP) yield { name: 'VIP', url: '/vip' };
}());
}
より初学者向けに展開すると次のようにも書けます。
function* buildMenu(user) { /* 省略 */ }
const iter = buildMenu(user);
const arr = Array.from(iter);
実務ユースケース2:巨大ログをストリームで処理しメモリを節約(Node.js)
数十万行のログを一括読み込みするとメモリが枯渇します。Node.js の readline
を例に、1 行ずつパースしながら DB に書き込むパイプラインを示します。
import { createReadStream } from 'fs';
import * as readline from 'readline';
async function\* parseLog(path) {
const rl = readline.createInterface({
input: createReadStream(path),
crlfDelay: Infinity,
});
for await (const line of rl) {
const \[ts, level, msg] = line.split('\t');
yield { ts, level, msg };
}
}
for await (const record of parseLog('access.log')) {
await db.insert(record); // 逐次インサートでピークメモリ一定
}
Async Generator で非同期フロー──for await…of の実力
async function*
を使うと await
と yield
を同時に扱えます。下記は外部 API を順番に呼び出しつつ結果をストリームする例です。
async function* fetchPages(urls) {
for (const url of urls) {
const res = await fetch(url);
yield await res.json();
}
}
for await (const page of fetchPages(list)) {
console.log(page.title);
}
Async Generator は ES2018 で標準化され、主要ブラウザ(Chrome 63+、Firefox 57+、Safari 11.1+、Edge 79+)で利用できます。
パフォーマンスと制限事項──測定とベストプラクティス
ジェネレータは呼び出しごとにオブジェクトを生成するため、ミリ秒以下の最適化が必要なループでは純粋な配列操作より遅くなる場合があります。簡易ベンチマーク例を示します(Node 18、100 万回ループ)。
// for ループ: 約30 ms
let sum = 0;
for (let i = 0; i < 1e6; i++) sum += i;
// ジェネレータ: 約65 ms
function* counter(n) { for (let i = 0; i < n; i++) yield i; }
sum = 0;
for (const v of counter(1e6)) sum += v;
速度差は環境によって変わるので、自前のコードで計測し判断する姿勢が大切です。また、ジェネレータは一度消費すると再利用できません。必要なら関数呼び出しをやり直すか、配列に展開して保持してください。
例外を外側から投げ込みたい場合は iterator.throw(err)
を使えますが、エラーハンドリングを try/catch
内に置く設計が望ましいです。
JavaScriptの関数はなぜ「オブジェクト」なのか?仕様と設計思想から理解する
まとめ:ジェネレータ関数で実装を“読みやすく・軽く”
ジェネレータ関数は「処理の一時停止と再開」「副作用の制御」「ストリームとの親和性」を兼ね備え、実務でこそ威力を発揮します。
複雑化した配列生成をすっきりさせたり、大容量データを安全に処理したりと、現場の課題に直結するテクニックです。
まずは小さなリファクタリングで導入し、効果とチームの受け入れやすさを確認してみてください。きっと開発体験が一段レベルアップするはずです。