【実務で使える】JavaScriptジェネレータ関数入門|配列生成・ストリーム処理の最適解

【実務で使える】JavaScriptジェネレータ関数入門|配列生成・ストリーム処理の最適解

【実務で使える】JavaScriptジェネレータ関数入門|配列生成・ストリーム処理の最適解

JavaScript の function*yield が生み出すジェネレータ関数は、一見するとマニアックです。しかし配列の動的生成やストリーミング処理など、実務でありがちな“かゆい所”を解消してくれる便利ツールでもあります。

この記事では「JavaScript × ジェネレータ関数 × 実務」を軸に、仕組みから応用パターン、落とし穴まで掘り下げます。

目次

ジェネレータ関数の基本としくみ──Generator オブジェクトと next()

ジェネレータ関数を呼び出すと即時実行はされず、Generator オブジェクト(Iterator を実装したオブジェクト)が返ります。

戻り値の next(){ value, done } 形式のレコードを返し、donetrue になった時点で反復は終了です。

さらに 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* を使うと awaityield を同時に扱えます。下記は外部 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の関数はなぜ「オブジェクト」なのか?仕様と設計思想から理解する

まとめ:ジェネレータ関数で実装を“読みやすく・軽く”

ジェネレータ関数は「処理の一時停止と再開」「副作用の制御」「ストリームとの親和性」を兼ね備え、実務でこそ威力を発揮します。

複雑化した配列生成をすっきりさせたり、大容量データを安全に処理したりと、現場の課題に直結するテクニックです。

まずは小さなリファクタリングで導入し、効果とチームの受け入れやすさを確認してみてください。きっと開発体験が一段レベルアップするはずです。

なぜJavaScriptでは関数をconstで定義するのか?functionとの違い・メリット・使い分け

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次