JavaScript の Iterator と Generator は、for...of や spread 構文を支える言語の根幹プロトコルでありながら、実務で意識的に使いこなしているエンジニアは意外と少ない領域です。「なんとなく for...of で回しているけど、裏で何が動いているのか説明できない」「function* は知っているが現場でどう使うかピンとこない」――そんな状態を、本記事で完全に解消します。
本記事では ECMAScript 2025 / Node.js 22+ / TypeScript 5.x 準拠で、コピペで動く 40 以上のコードサンプルを通して、Iterator プロトコル → Iterable プロトコル → Symbol.iterator 自前実装 → Generator 関数 → yield / yield* → 無限シーケンス・lazy 評価 → Async Generator → for await...of → ページネーション・Stream 連携 → ES2025 Iterator Helpers(map / filter / take)→ パフォーマンス・TypeScript 型まで、Iterator/Generator の実践知見を完全網羅します。
本記事は「Iteration プロトコル」観点に特化しているため、Promise 完全実践ガイド(非同期処理の観点)、async/await 完全実践ガイド(async 構文の観点)、および JavaScript 配列メソッド完全ガイド(配列操作の観点)と相互補完的に読むと、JavaScript のデータ走査周りの基礎体力が一気に上がります。
1. Iterator / Iterable プロトコルの全体像
JavaScript の for...of / spread / Array.from / 分割代入が動く仕組みは、すべて Iterable プロトコル と Iterator プロトコルの 2 段構成で説明できます。この 2 つを切り分けて理解できると、Generator も Async Iterator も一気に腹落ちします。
1.1 Iterator プロトコルの定義
Iterator は next() を呼ぶたびに { value, done } を返すオブジェクトです。done: true が来た時点で走査終了です。
// Iterator プロトコル: next() が { value, done } を返すオブジェクト
const iterator = {
i: 0,
next() {
if (this.i < 3) {
return { value: this.i++, done: false };
}
return { value: undefined, done: true };
},
};
console.log(iterator.next()); // { value: 0, done: false }
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: undefined, done: true }
1.2 Iterable プロトコルの定義
Iterable は [Symbol.iterator]() メソッドを持つオブジェクトで、これを呼ぶと Iterator が返ります。for…of は Iterable しか回せません(Iterator 単体は回せない)。
// Iterable プロトコル: [Symbol.iterator]() で Iterator を返す
const iterable = {
[Symbol.iterator]() {
let i = 0;
return {
next() {
return i < 3
? { value: i++, done: false }
: { value: undefined, done: true };
},
};
},
};
for (const v of iterable) {
console.log(v); // 0, 1, 2
}
1.3 組込 Iterable の確認
Array / String / Map / Set / arguments / NodeList / TypedArray などはすべて Iterableです。[Symbol.iterator] が存在することを確認できます。
// 組込 Iterable の確認
console.log(typeof [][Symbol.iterator]); // 'function'
console.log(typeof ''[Symbol.iterator]); // 'function'
console.log(typeof new Map()[Symbol.iterator]); // 'function'
console.log(typeof new Set()[Symbol.iterator]); // 'function'
// 一方、ふつうのオブジェクトは Iterable ではない
console.log(typeof ({})[Symbol.iterator]); // 'undefined'
// だから for...of で {} は回せない
// for (const v of { a: 1 }) {} // TypeError
1.4 Iterator が Iterable も兼ねる慣習
実用上は [Symbol.iterator]() { return this; } を生やしてIterator 自身を Iterable にもするパターンが標準です。Generator も内部的にこの形になっています。
// Iterator 兼 Iterable パターン
function makeRange(start, end) {
let i = start;
return {
next() {
return i < end
? { value: i++, done: false }
: { value: undefined, done: true };
},
// self-iterable: これがあると for...of で直接回せる
[Symbol.iterator]() {
return this;
},
};
}
for (const n of makeRange(0, 3)) {
console.log(n); // 0, 1, 2
}
2. 自前 Iterator を実装する
Iterator プロトコルが分かったら、実際に自作クラスを Iterable にしてみるのが理解への最短ルートです。範囲(Range)・連結リスト・ツリーなど、独自データ構造に for...of を生やせるようになります。
2.1 Range クラスを Iterable にする
Python の range 相当を JavaScript で書く例です。クラスに [Symbol.iterator]() を生やすだけで for...of ・spread ・Array.from がすべて動きます。
class Range {
constructor(start, end, step = 1) {
this.start = start;
this.end = end;
this.step = step;
}
[Symbol.iterator]() {
let i = this.start;
const end = this.end;
const step = this.step;
return {
next() {
if (i >= end) return { value: undefined, done: true };
const value = i;
i += step;
return { value, done: false };
},
};
}
}
for (const n of new Range(0, 5)) {
console.log(n); // 0, 1, 2, 3, 4
}
2.2 spread / Array.from / 分割代入の連携
Iterable にした瞬間、spread / Array.from / 分割代入がすべて自動で動くのが Iteration プロトコルの最大のメリットです。
const range = new Range(10, 15);
// spread
console.log([...range]); // [10, 11, 12, 13, 14]
// Array.from(第 2 引数で map もできる)
console.log(Array.from(new Range(10, 15), x => x * 2));
// → [20, 22, 24, 26, 28]
// 分割代入(Iterable 対応)
const [a, b, ...rest] = new Range(10, 15);
console.log(a, b, rest); // 10 11 [12, 13, 14]
// Set / Map のコンストラクタも Iterable を受け取る
console.log(new Set(new Range(10, 15))); // Set(5) { 10, 11, 12, 13, 14 }
2.3 連結リスト(LinkedList)を Iterable にする
独自データ構造を Iterable にする実例です。head から next をたどるだけで、利用側からは普通の配列のように扱えます。ここでは Generator メソッド構文(*[Symbol.iterator]())を先取りで使っています。
class LinkedList {
constructor() { this.head = null; }
push(value) {
const node = { value, next: null };
if (!this.head) this.head = node;
else {
let cur = this.head;
while (cur.next) cur = cur.next;
cur.next = node;
}
}
*[Symbol.iterator]() {
let cur = this.head;
while (cur) {
yield cur.value;
cur = cur.next;
}
}
}
const list = new LinkedList();
list.push('a'); list.push('b'); list.push('c');
console.log([...list]); // ['a', 'b', 'c']
2.4 ツリー(深さ優先探索)を Iterable にする
ツリー構造も Generator なら再帰 + yield*で 1 関数として書けます。DFS / BFS の切り替えがコード上で明確になります。
const tree = {
value: 1,
children: [
{ value: 2, children: [{ value: 4, children: [] }] },
{ value: 3, children: [{ value: 5, children: [] }] },
],
};
function* dfs(node) {
yield node.value;
for (const child of node.children) {
yield* dfs(child); // 再帰委譲
}
}
console.log([...dfs(tree)]); // [1, 2, 4, 3, 5]
3. Generator 関数(function*)入門
自前 Iterator を毎回書くのは面倒です。Generator 関数 function* を使えば、yield で値を出すだけで Iterable な関数を量産できます。本章は Generator の基本構文に集中します。
3.1 function* と yield の基本
Generator 関数は呼び出すと「実行されないまま Generator オブジェクトを返す」のがポイント。next() を呼んで初めて yield まで実行が進みます。
function* gen() {
console.log('start');
yield 1;
console.log('after yield 1');
yield 2;
console.log('after yield 2');
yield 3;
}
const g = gen();
console.log(g.next()); // 'start' → { value: 1, done: false }
console.log(g.next()); // 'after yield 1' → { value: 2, done: false }
console.log(g.next()); // 'after yield 2' → { value: 3, done: false }
console.log(g.next()); // { value: undefined, done: true }
3.2 Generator は Iterable も兼ねている
Generator が返すオブジェクトは Iterator かつ Iterable です。なので for...of ・spread ・Array.from がそのまま動きます。
function* gen() {
yield 'a'; yield 'b'; yield 'c';
}
// for...of
for (const v of gen()) {
console.log(v); // 'a', 'b', 'c'
}
// spread
console.log([...gen()]); // ['a', 'b', 'c']
// Array.from
console.log(Array.from(gen())); // ['a', 'b', 'c']
// 分割代入
const [first, ...others] = gen();
console.log(first, others); // 'a' ['b', 'c']
3.3 range を Generator で書き直す
2 章で書いた Range クラスの自前 Iterator が、Generator なら 3 行で同じ機能を実現できます。これが Generator の威力です。
function* range(start, end, step = 1) {
for (let i = start; i < end; i += step) {
yield i;
}
}
console.log([...range(0, 5)]); // [0, 1, 2, 3, 4]
console.log([...range(0, 10, 2)]); // [0, 2, 4, 6, 8]
console.log([...range(0, 3)]); // [0, 1, 2]
3.4 yield* で別の Iterable を委譲する
yield* は別の Iterable を「丸ごと展開して yield する」演算子です。Generator を組み合わせて構築する際に必須です。
function* abc() { yield 'a'; yield 'b'; yield 'c'; }
function* def() { yield 'd'; yield 'e'; yield 'f'; }
function* alphabet() {
yield* abc(); // 別 Generator を展開
yield* def();
yield* ['g', 'h']; // 配列(Iterable)も展開できる
}
console.log([...alphabet()]); // ['a','b','c','d','e','f','g','h']
3.5 Generator メソッド return() / throw()
Generator は next() 以外に return() と throw() を持ちます。それぞれ外部から終了させる / 例外を注入する用途で、リソース解放のテスト等で便利です。
function* gen() {
try {
yield 1;
yield 2;
yield 3;
} catch (e) {
console.log('caught:', e.message);
yield 99;
} finally {
console.log('finally');
}
}
// return() で強制終了(finally は走る)
const g1 = gen();
console.log(g1.next()); // { value: 1, done: false }
console.log(g1.return('end')); // 'finally' → { value: 'end', done: true }
// throw() で例外注入(catch 節で捕まる)
const g2 = gen();
console.log(g2.next()); // { value: 1, done: false }
console.log(g2.throw(new Error('x'))); // 'caught: x' → { value: 99, done: false }
3.6 next() に引数を渡して双方向通信する
あまり知られていない機能ですが、next(value) の引数は直前の yield 式全体の評価値になります。これで Generator と呼び出し側が双方向に値をやり取りできます。
function* dialog() {
const name = yield '名前は?';
const age = yield `${name} さん、年齢は?`;
return `${name}(${age}歳)で登録完了`;
}
const d = dialog();
console.log(d.next().value); // '名前は?'
console.log(d.next('taro').value); // 'taro さん、年齢は?'
console.log(d.next(30).value); // 'taro(30歳)で登録完了'
4. 無限シーケンスと lazy 評価
Generator の真価は「終わらないデータ列を扱える」点にあります。配列は全要素をメモリに展開しますが、Generator は必要な分だけ計算する lazy 評価が可能です。
4.1 無限の自然数列
配列では絶対に表現できない「自然数の無限列」も Generator なら表現できます。break や take で必要な分だけ取り出すのがポイントです。
function* naturals() {
let n = 0;
while (true) {
yield n++;
}
}
// 必ず break か take で停止させる
const taken = [];
for (const n of naturals()) {
if (n >= 5) break;
taken.push(n);
}
console.log(taken); // [0, 1, 2, 3, 4]
4.2 フィボナッチ数列
無限フィボナッチも 6 行で書けます。再帰や DP テーブルが不要になるのが Generator の魅力です。
function* fibonacci() {
let [a, b] = [0, 1];
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
// 最初の 10 項
const fib10 = [];
for (const v of fibonacci()) {
if (fib10.length >= 10) break;
fib10.push(v);
}
console.log(fib10); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
4.3 take ユーティリティ
Iterable から先頭 N 件だけ取り出す take を自作してみます。これ自体も Generator として書けるのがエレガントなところです。
function* take(iterable, n) {
let i = 0;
for (const v of iterable) {
if (i++ >= n) break;
yield v;
}
}
console.log([...take(naturals(), 5)]); // [0, 1, 2, 3, 4]
console.log([...take(fibonacci(), 7)]); // [0, 1, 1, 2, 3, 5, 8]
4.4 lazy フィルタ・map で巨大データを扱う
配列の filter → map → slice(0, 5) はすべての中間配列をメモリに展開しますが、Generator 版なら必要な 5 件しか計算されません。1 億件のデータでも O(必要件数)で済みます。
function* gFilter(iterable, pred) {
for (const v of iterable) if (pred(v)) yield v;
}
function* gMap(iterable, fn) {
for (const v of iterable) yield fn(v);
}
// 0..∞ から、偶数を 2 倍して先頭 5 件
const result = [...take(
gMap(gFilter(naturals(), x => x % 2 === 0), x => x * 2),
5
)];
console.log(result); // [0, 4, 8, 12, 16]
// → 実際に評価されたのは 10 個程度。1 億件回しても OK。
5. Generator でパイプラインを組む
Generator はネスト関数呼び出しが見づらくなりがちです。パイプ風ユーティリティを 1 つ用意すると、配列メソッドチェーンと同じ書き味で lazy 処理が書けます。
5.1 pipe ユーティリティの定義
関数合成のパイプ関数を作ります。可変長引数で左から右に適用するだけの単純な実装ですが、効果絶大です。
const pipe = (...fns) => (input) => fns.reduce((acc, fn) => fn(acc), input);
const filterP = (pred) => (it) => gFilter(it, pred);
const mapP = (fn) => (it) => gMap(it, fn);
const takeP = (n) => (it) => take(it, n);
const flow = pipe(
filterP(x => x % 2 === 0),
mapP(x => x * x),
takeP(5),
);
console.log([...flow(naturals())]); // [0, 4, 16, 36, 64]
5.2 reduce 相当の sum / count
Generator は yield する側だけが lazy なので、最後の集約は通常の関数で OK。最終消費に for...of + アキュムレータを使うのが定番です。
function sum(iterable) {
let s = 0;
for (const v of iterable) s += v;
return s;
}
function count(iterable) {
let c = 0;
for (const _ of iterable) c++;
return c;
}
console.log(sum(take(naturals(), 100))); // 4950 = 0+1+...+99
console.log(count(take(fibonacci(), 20))); // 20
5.3 zip(複数 Iterable を並べる)
2 つ以上の Iterable を「同じインデックスでペアにする」ユーティリティです。どれか 1 つでも終わったら全体を終了させるのが基本仕様です。
function* zip(...iterables) {
const iters = iterables.map(it => it[Symbol.iterator]());
while (true) {
const results = iters.map(it => it.next());
if (results.some(r => r.done)) return;
yield results.map(r => r.value);
}
}
const names = ['Alice', 'Bob', 'Carol'];
const ages = [30, 25, 28];
console.log([...zip(names, ages)]);
// [['Alice', 30], ['Bob', 25], ['Carol', 28]]
5.4 chunk(N 件ごとに区切る)
巨大な Iterable を N 件ごとのバッチに分割する処理も Generator で書けばメモリ効率良く処理できます。バッチ INSERT などで多用します。
function* chunk(iterable, size) {
let buf = [];
for (const v of iterable) {
buf.push(v);
if (buf.length === size) {
yield buf;
buf = [];
}
}
if (buf.length) yield buf;
}
console.log([...chunk(range(0, 7), 3)]);
// [[0,1,2], [3,4,5], [6]]
6. Async Generator と for await…of
同期 Generator までは「便利な道具」レベルですが、Async Generator は実務で必須のレベル感です。ページネーション API・Stream・WebSocket・DB カーソルなど、少しずつ非同期に届くデータを統一的に扱えます。
6.1 async function* の基本
async function* は yield と await を両方使える Generator です。返り値は Promise<{value, done}> を返す Async Iterator になります。
async function* asyncGen() {
yield 'a';
await new Promise(r => setTimeout(r, 100));
yield 'b';
await new Promise(r => setTimeout(r, 100));
yield 'c';
}
// for await...of で受ける(中身は Promise が解決される)
for await (const v of asyncGen()) {
console.log(v); // 'a' → 100ms → 'b' → 100ms → 'c'
}
6.2 for await…of の使いどころ
for await...of はAsync Iterable / Promise の配列 / 同期 Iterable の Promise をすべて受けられます。Promise.all との違いは「順次・lazy」な点です。
// Promise の配列も for await...of で順次解決できる
const tasks = [
Promise.resolve(1),
new Promise(r => setTimeout(() => r(2), 50)),
Promise.resolve(3),
];
for await (const v of tasks) {
console.log(v); // 1, 2, 3(到着順ではなく、配列順)
}
// 同期 Iterable も OK
for await (const v of [10, 20, 30]) {
console.log(v); // 10, 20, 30
}
6.3 ページネーション API を Async Generator にする
API のカーソル / page=N ページネーションを Async Generator にすると、利用側は for await...of だけで全件を取り出せます。これが Async Generator の最大の実務メリットです。
async function* fetchAllUsers(perPage = 100) {
let page = 1;
while (true) {
const res = await fetch(`/api/users?page=${page}&per_page=${perPage}`);
const data = await res.json();
for (const user of data.items) {
yield user;
}
if (data.items.length < perPage) return; // 最終ページ
page++;
}
}
// 利用側は全 1 万件でも書き味は同じ
for await (const user of fetchAllUsers()) {
console.log(user.id, user.name);
}
6.4 Node.js Readable Stream を Iterable として扱う
Node.js 12+ の Readable Stream は Async Iterableです。巨大ファイルを for await...of でチャンク処理できるので、コードが激減します。
// Node.js: fs.createReadStream は Async Iterable
import { createReadStream } from 'node:fs';
const stream = createReadStream('./huge.log', { encoding: 'utf8' });
let totalLines = 0;
let buf = '';
for await (const chunk of stream) {
buf += chunk;
const lines = buf.split('n');
buf = lines.pop() ?? ''; // 最終行は次のチャンクに繰越
totalLines += lines.length;
}
console.log('lines:', totalLines);
6.5 Web 標準 ReadableStream → Async Iterator
Web 標準の ReadableStream も2024 年以降 Async Iterableとして規格化されました。fetch().body を for await...of で消費できます。
// ブラウザ / Node.js 22+ / Deno / Bun で動く
const res = await fetch('https://example.com/big.json');
const decoder = new TextDecoder();
let text = '';
for await (const chunk of res.body) {
text += decoder.decode(chunk, { stream: true });
}
text += decoder.decode();
console.log('total bytes:', text.length);
6.6 Async Generator で並列化(並行ダウンロード)
Async Generator は原則「順次」です。並列化したい場合は Promise.all + chunk を組み合わせるのが鉄板です。async/await 完全実践ガイドの並列パターンと併せて読むと理解が深まります。
async function* downloadMany(urls, concurrency = 4) {
for (const batch of chunk(urls, concurrency)) {
const results = await Promise.all(
batch.map(u => fetch(u).then(r => r.text()))
);
yield* results;
}
}
const urls = ['https://example.com/1', 'https://example.com/2'];
for await (const body of downloadMany(urls, 4)) {
console.log(body.length);
}
7. ES2025 Iterator Helpers
ES2025 で待望の Iterator Helpers が標準化されました。Iterator に直接 .map() / .filter() / .take() / .drop() / .toArray() が生え、Generator が配列メソッドと同じ書き味で扱えます。
7.1 Iterator.prototype.map / filter
Generator 戻り値に対し、配列メソッドと同じ感覚で map / filter が直接呼べるようになりました。すべて lazyなのが配列メソッドとの最大の違いです。
// ES2025: Iterator Helpers
// Node.js 22+ / Chrome 122+ / Safari 18+ で利用可
function* naturals() { let n = 0; while (true) yield n++; }
const result = naturals()
.filter(x => x % 2 === 0)
.map(x => x * x)
.take(5)
.toArray();
console.log(result); // [0, 4, 16, 36, 64]
7.2 take / drop / forEach / reduce / some / every
Iterator Helpers は配列メソッドとほぼ同じラインナップを揃えています。無限 Iterable に対しても安全に使える(take で停止できる)のがポイント。
// drop: 先頭 N 件を捨てる
console.log(naturals().drop(10).take(3).toArray()); // [10, 11, 12]
// forEach: 副作用専用(配列を作らない)
naturals().take(3).forEach(v => console.log(v)); // 0, 1, 2
// reduce
console.log(naturals().take(10).reduce((a, b) => a + b, 0)); // 45
// some / every(短絡評価)
console.log(naturals().take(100).some(v => v === 50)); // true
console.log(naturals().take(100).every(v => v < 100)); // true
7.3 Async Iterator Helpers
同じ API が Async Iterator にも生えます。Async Generator を配列メソッド感覚で扱えるようになり、コード量が劇的に減ります。
async function* fetchAllUsers() { /* 前述 */ }
// 1 万人の中から、アクティブな上位 50 件の名前だけ集める
const top50 = await fetchAllUsers()
.filter(u => u.active)
.map(u => u.name)
.take(50)
.toArray();
console.log(top50.length); // 50(以下)
7.4 polyfill / 互換性確認
古いブラウザや Node.js 20 以前では core-js の polyfill を入れるのが定番です。サーバ側だけなら問題ありませんが、フロントは要対応です。
// package.json
// "dependencies": { "core-js": "^3.36.0" }
// エントリポイントの先頭で
import 'core-js/proposals/iterator-helpers';
import 'core-js/proposals/async-iterator-helpers';
// あとは ES2025 と同じ書き方で OK
[1, 2, 3].values().map(x => x * 2).toArray(); // [2, 4, 6]
// 機能検出
if (typeof Iterator !== 'undefined' && Iterator.prototype.map) {
console.log('Iterator Helpers ネイティブ対応');
}
8. 組込 Iterator の活用
JavaScript の組込オブジェクトには Iterator がすでに大量に仕込まれています。Map / Set / Object.entries / String など、知っておくと普段のコードが圧倒的に綺麗になるものを実例で見ていきます。
8.1 Map / Set の組込 Iterator
Map / Set には keys() / values() / entries() がすべて Iterator として用意されています。ES2025 では Iterator Helpers が直接適用できます。
const map = new Map([['a', 1], ['b', 2], ['c', 3]]);
// for...of でエントリを回す
for (const [k, v] of map) console.log(k, v); // 'a' 1 / 'b' 2 / 'c' 3
// 個別の Iterator
console.log([...map.keys()]); // ['a', 'b', 'c']
console.log([...map.values()]); // [1, 2, 3]
console.log([...map.entries()]); // [['a',1],['b',2],['c',3]]
// ES2025 では Iterator Helpers が直接使える
const sum = map.values().reduce((a, b) => a + b, 0);
console.log(sum); // 6
8.2 Object.entries / Object.values / Object.keys
素のオブジェクトは Iterable ではないので、Object.entries() 等で配列に変換してから for…of する必要があります。これは ES2017 以来の定石です。
const user = { name: 'taro', age: 30, role: 'admin' };
for (const [key, value] of Object.entries(user)) {
console.log(`${key}: ${value}`);
}
// name: taro / age: 30 / role: admin
// ES2024 の Object.groupBy も Iterable を受ける
const items = [
{ type: 'fruit', name: 'apple' },
{ type: 'veg', name: 'carrot' },
{ type: 'fruit', name: 'orange' },
];
console.log(Object.groupBy(items, i => i.type));
// { fruit: [...], veg: [...] }
8.3 String の文字単位 Iterator
String の Iterator はサロゲートペアを 1 文字として扱う賢い動きをします。"𠮷".length === 2 でも [..."𠮷"].length === 1 です。
const s = 'a𠮷b'; // '𠮷' はサロゲートペア(2 code unit)
console.log(s.length); // 4(code unit 数)
console.log([...s].length); // 3(コードポイント数)
console.log([...s]); // ['a', '𠮷', 'b']
// for...of もコードポイント単位で回る
for (const ch of '🍣🍺🍙') console.log(ch); // '🍣' / '🍺' / '🍙'
8.4 NodeList / FileList / FormData / Headers
DOM 系コレクションも近年 Iterable 対応が進みました。querySelectorAll や FormData も for...of と spread が使えます。
// NodeList(querySelectorAll の戻り値)も Iterable
for (const el of document.querySelectorAll('a')) {
console.log(el.href);
}
const links = [...document.querySelectorAll('a')]; // 配列化
// FormData は entries() で回せる
const fd = new FormData(document.querySelector('form'));
for (const [name, value] of fd) console.log(name, value);
// Headers, URLSearchParams も同様
const params = new URLSearchParams('a=1&b=2');
for (const [k, v] of params) console.log(k, v);
9. パフォーマンスとメモリ効率
Generator は「常に速い」わけではありません。「メモリ効率」と「実行速度」はトレードオフです。配列メソッド vs Generator のベンチを取って、使い分けを身につけましょう。
9.1 メモリ消費の比較
1 億件の数列を扱う比較です。配列はメモリを 800MB 以上消費してクラッシュしますが、Generator は数 KB しか使いません。
// (危険) 配列で 1 億件 → OOM の可能性
// const arr = Array.from({ length: 1e8 }, (_, i) => i);
// (安全) Generator なら数 KB
function* big(n) {
for (let i = 0; i < n; i++) yield i;
}
let sum = 0;
for (const v of big(1e8)) {
sum += v;
if (v >= 1000) break; // ここで停止すれば 1KB も使わない
}
console.log(sum); // 500500
9.2 実行速度のベンチマーク
逆に全件処理する場合は配列メソッドの方が高速です。Generator は next() 呼び出しコストがあるため、CPU バウンドな処理では 2〜5 倍程度遅くなります。
const N = 1_000_000;
const arr = Array.from({ length: N }, (_, i) => i);
console.time('array');
const a = arr.filter(x => x % 2 === 0).map(x => x * 2).reduce((a, b) => a + b, 0);
console.timeEnd('array'); // 例: 30ms
console.time('generator');
function* gen() { for (const v of arr) yield v; }
let g = 0;
for (const v of gen()) if (v % 2 === 0) g += v * 2;
console.timeEnd('generator'); // 例: 80ms(配列の 2〜3 倍遅い)
9.3 使い分け基準
実務での使い分けは以下の通り。「全件 vs 部分」「メモリ vs 速度」の 2 軸で判断します。
// ✅ Generator 向き(lazy / 早期終了 / 巨大データ / 非同期)
// - find / some / every で短絡したい
// - 1 億件のうち先頭 100 件だけ欲しい
// - ページネーション API の全件走査
// - Stream / ログファイル処理
// - 無限シーケンス
// ✅ 配列向き(全件処理 / 複数回走査 / インデックスアクセス)
// - 全件 map / filter / reduce
// - 同じデータを何度も走査
// - arr[i] でランダムアクセス
// - 並列 Promise.all
// 迷ったら「最後に toArray() するなら配列で書く」「短絡 / 無限なら Generator」
10. TypeScript と Iterator/Generator
TypeScript には Iterator/Generator 関連の組込型がしっかり用意されています。Iterable<T> / Iterator<T> / Generator<T, TReturn, TNext> を使いこなせば、型安全な lazy 処理が書けます。
10.1 基本の組込型
TypeScript の lib.es2015.iterable.d.ts に主要型がまとまっています。受け取り側は Iterable<T>、返す側は Generator<T> が基本形です。
// Generator 関数の戻り値型
function* numbers(): Generator<number, void, undefined> {
yield 1; yield 2; yield 3;
}
// 受け取り側は Iterable<T> で OK
function sum(it: Iterable<number>): number {
let s = 0;
for (const v of it) s += v;
return s;
}
sum(numbers()); // OK
sum([1, 2, 3]); // OK
sum(new Set([1,2])); // OK
10.2 Generator<T, TReturn, TNext> の 3 型引数
Generator 型は 3 引数を取ります。yield する値 / return する値 / next() に渡す値の型を個別に表現できます。
// T = yield する型 / TReturn = return する型 / TNext = next() に渡す型
function* dialog(): Generator<string, number, string> {
const name = yield '名前は?'; // TNext = string なので name: string
const age = yield `${name} さん年齢は?`;
return Number(age) || 0; // TReturn = number
}
const d = dialog();
const q1 = d.next(); // { value: string, done: false }
const q2 = d.next('taro'); // { value: string, done: false }
const r = d.next('30'); // { value: number, done: true }
console.log(r.value); // 30(number 型)
10.3 AsyncIterable / AsyncGenerator
Async 系もまったく同じ命名規則の型が用意されています。for await...of で受ける関数の引数型は AsyncIterable<T> が基本です。
async function* fetchUsers(): AsyncGenerator<{ id: number; name: string }> {
let page = 1;
while (true) {
const res = await fetch(`/api/users?page=${page}`);
const data = await res.json() as { items: { id: number; name: string }[] };
for (const u of data.items) yield u;
if (data.items.length === 0) return;
page++;
}
}
async function collectNames(it: AsyncIterable<{ name: string }>): Promise<string[]> {
const names: string[] = [];
for await (const u of it) names.push(u.name);
return names;
}
10.4 Iterable をジェネリック関数で扱う
ユーティリティ関数を書くときは戻り値も Generator にしてジェネリックにするのが定石です。型推論が効くので呼び出し側は何も書かなくて OK になります。
function* take<T>(iterable: Iterable<T>, n: number): Generator<T> {
let i = 0;
for (const v of iterable) {
if (i++ >= n) return;
yield v;
}
}
const first3 = [...take([10, 20, 30, 40, 50], 3)]; // number[]
const first2 = [...take(['a', 'b', 'c'], 2)]; // string[]
// 型が自動推論される
10.5 tsconfig での downlevelIteration
ES5 ターゲットで Iterator を使う場合は downlevelIteration: true が必要です。target が ES2015 以上なら不要。tsconfig 完全ガイドと併せて確認してください。
// tsconfig.json
{
"compilerOptions": {
"target": "ES2020", // ES2015 以上なら downlevelIteration 不要
"lib": ["ES2025", "DOM"], // Iterator Helpers を使うなら ES2025
"downlevelIteration": true // target が ES5 のときのみ true
}
}
11. 実務パターン集
最後に、現場で「これは Generator の出番だな」と判断できる実務頻出パターンを一気にまとめます。コピペで応用できるものばかりです。
11.1 リトライ付きフェッチを Async Generator で
失敗時に指数バックオフでリトライする処理を Async Generator で表現すると、呼び出し側は for await...of で書けるのでとても綺麗になります。
async function* fetchWithRetry(url, maxRetry = 3) {
for (let attempt = 0; attempt <= maxRetry; attempt++) {
try {
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
yield await res.json();
return;
} catch (e) {
if (attempt === maxRetry) throw e;
const wait = 1000 * 2 ** attempt;
console.warn(`retry in ${wait}ms`, e.message);
await new Promise(r => setTimeout(r, wait));
}
}
}
for await (const data of fetchWithRetry('/api/data')) {
console.log(data);
}
11.2 タイムアウト付き Iterator
長時間動く Generator にタイムアウトを被せるパターン。AbortSignal を組み合わせるのが ES2024 以降の定石です。
async function* withTimeout(iterable, ms) {
const it = iterable[Symbol.asyncIterator]?.() ?? iterable[Symbol.iterator]();
while (true) {
const next = Promise.resolve(it.next());
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error('timeout')), ms)
);
const { value, done } = await Promise.race([next, timeout]);
if (done) return;
yield value;
}
}
for await (const v of withTimeout(fetchAllUsers(), 5000)) {
console.log(v);
}
11.3 AbortSignal でキャンセル可能にする
AbortController + signal.aborted を確認することで、Generator を外部から停止できます。React の useEffect クリーンアップなどで有用です。
async function* poll(url, intervalMs, signal) {
while (!signal.aborted) {
const res = await fetch(url, { signal });
yield await res.json();
await new Promise(r => setTimeout(r, intervalMs));
}
}
const ctrl = new AbortController();
setTimeout(() => ctrl.abort(), 10_000); // 10 秒で止める
try {
for await (const data of poll('/api/status', 1000, ctrl.signal)) {
console.log(data);
}
} catch (e) {
if (e.name !== 'AbortError') throw e;
}
11.4 巨大 CSV の lazy パース
数 GB の CSV を全件メモリ展開せず、1 行ずつ Generator で yield するパターン。Node.js のログ集計などで非常によく使います。
import { createReadStream } from 'node:fs';
import { createInterface } from 'node:readline';
async function* parseCsv(path) {
const rl = createInterface({ input: createReadStream(path) });
let headers = null;
for await (const line of rl) {
const cols = line.split(',');
if (!headers) { headers = cols; continue; }
yield Object.fromEntries(headers.map((h, i) => [h, cols[i]]));
}
}
let count = 0;
for await (const row of parseCsv('./users.csv')) {
if (row.active === 'true') count++;
}
console.log('active users:', count);
11.5 Coroutine 風の非同期制御(歴史)
async/await が無かった ES6 時代、Generator + Promise で「co パターン」と呼ばれる擬似 await が広く使われていました。現在は async/await があるので新規採用は不要ですが、レガシーコードで見かけたら理解できるように。
// co パターン: Generator で Promise を yield → ランナーが解決して next に戻す
function co(genFn) {
return new Promise((resolve, reject) => {
const g = genFn();
const step = (input) => {
const { value, done } = g.next(input);
if (done) return resolve(value);
Promise.resolve(value).then(step, reject);
};
step();
});
}
co(function* () {
const a = yield Promise.resolve(1);
const b = yield Promise.resolve(2);
return a + b;
}).then(console.log); // 3
// 現在は async/await で同じことが書ける → 詳細は Promise / async ガイド参照
12. まとめ
JavaScript の Iterator と Generator は、for...of から ES2025 Iterator Helpers / Async Generator / Stream まで、言語の根幹を貫くプロトコルです。本記事のポイントを再整理します。
- Iterator =
next()を持つオブジェクト、Iterable =[Symbol.iterator]()を持つオブジェクト。for...ofは Iterable しか回せない - 自作クラスに
[Symbol.iterator]()を生やすだけでfor...of・spread ・Array.fromがすべて動く function*+yieldで Iterator を 3 行で生成可能。yield*で別の Iterable を委譲できる- Generator は無限シーケンスとlazy 評価が可能。1 億件のうち先頭 100 件だけ計算する、といった処理が自然に書ける
- Async Generator +
for await...ofはページネーション API / Stream / WebSocket の必須技。実務で頻出 - ES2025 Iterator Helpers(
map/filter/take/toArray)で、Generator が配列メソッドと同じ書き味になる - パフォーマンスは「全件処理なら配列 / 短絡・無限なら Generator」。メモリと速度のトレードオフを判断
- TypeScript は
Iterable<T>/Generator<T, TReturn, TNext>/AsyncIterable<T>で型安全に扱える
本記事はIteration プロトコル単体に絞った解説でした。非同期の全体像は Promise 完全実践ガイド と async/await 完全実践ガイド で、配列メソッドとの使い分けは JavaScript 配列メソッド完全ガイド で詳細に解説していますので、合わせて読むと JavaScript のデータ処理スキルがもう一段深まります。
Iterator/Generator を 「単なる構文」ではなく「lazy なデータ処理パイプライン」として扱えるようになれば、巨大ログ集計・API 全件取得・Stream 処理のコードが一気にスリムになります。本記事のコードはすべて Node.js 22+ / Chrome 122+ / Safari 18+ で動作確認可能ですので、ぜひ手元で動かして体得してください。

コメント