イテラブルとイテレータ
イテレータとは?
イテレータ(iterator) はひとことで言うと、「next() メソッドで 次の値を順番に取り出せるオブジェクト」です。
イテレータは、次の形のオブジェクトを返す next() を持ちます:
{ value: 任意の値, done: boolean }done: false→ まだ続きがあるdone: true→ もう値は終わり
手作りイテレータの例:
function createCounterIterator(max) {
let current = 1;
return {
next() {
if (current <= max) {
return { value: current++, done: false };
} else {
return { value: undefined, done: true };
}
}
};
}
const it = createCounterIterator(3);
console.log(it.next()); // { value: 1, done: false }
console.log(it.next()); // { value: 2, done: false }
console.log(it.next()); // { value: 3, done: false }
console.log(it.next()); // { value: undefined, done: true }イテラブルとは?
イテラブル(iterable) は、イテレータを「生成できる」オブジェクトです。
「Symbol.iterator という特別なキーのメソッドを持っていて、それを呼ぶとイテレータが返ってくるオブジェクト」が イテラブル です。
const iterable = {
[Symbol.iterator]() {
// ここで「イテレータ」を返す
let current = 1;
const max = 3;
return {
next() {
if (current <= max) {
return { value: current++, done: false };
} else {
return { value: undefined, done: true };
}
}
};
}
};iterable は、Symbol.iterator メソッドを持っているので イテラブル です。
そして、iterable[Symbol.iterator]() が返すオブジェクトが イテレータ です。
どこで使われるのか?
JavaScriptの イテラブルプロトコル は、次のところで使われています。
for...ofループ- スプレッド構文
...(配列リテラル、関数引数など) - 配列分割代入(
const [a, b] = iterable;) Array.from(iterable)Promise.all(iterable)など、一部の標準関数の引数
たとえば:
const arr = [10, 20, 30];
for (const value of arr) {
console.log(value);
}これは内部的に:
const iterator = arr[Symbol.iterator]();- ループごとに
iterator.next()を呼ぶ done: trueになるまで続ける
という動きをしています。
配列だけでなく、文字列・Map・Set・arguments・多くのDOMコレクション もイテラブルです。
組み込みイテラブルの例と for…of の挙動
配列
const arr = ["a", "b", "c"];
console.log(typeof arr[Symbol.iterator]); // "function"
for (const value of arr) {
console.log(value); // "a" "b" "c"
}文字列
const s = "ABC";
for (const ch of s) {
console.log(ch); // "A" "B" "C"
}Map / Set
const set = new Set([1, 2, 3]);
for (const v of set) {
console.log(v); // 1, 2, 3
}
const map = new Map([
["a", 1],
["b", 2]
]);
for (const [key, value] of map) {
console.log(key, value); // "a" 1 / "b" 2
}for...in との違い
for...in- 「列挙可能なプロパティ名」 を反復する
- オブジェクト用(キー列挙)
for...of- 「イテラブルからの値」 を反復する
- 配列・文字列・Map・Set などに使う
const arr = ["a", "b"];
for (const i in arr) {
console.log("in:", i); // "0", "1"
}
for (const v of arr) {
console.log("of:", v); // "a", "b"
}自作イテラブルを実装してみる
シンプルなカウンターイテラブル
const counter = {
from: 1,
to: 5,
[Symbol.iterator]() {
let current = this.from;
const last = this.to;
return {
next() {
if (current <= last) {
return { value: current++, done: false };
} else {
return { value: undefined, done: true };
}
}
};
}
};
for (const n of counter) {
console.log(n); // 1 2 3 4 5
}ポイント:
Symbol.iteratorは 毎回「新しいイテレータ」 を返すべきfor...ofはcounter[Symbol.iterator]()を呼んで、返ってきたイテレータでループしている
無限イテレータ
const infinite = {
[Symbol.iterator]() {
let current = 1;
return {
next() {
return { value: current++, done: false }; // doneがfalseのまま
}
};
}
};
// for (const n of infinite) { ... } は無限ループになるので注意
const it = infinite[Symbol.iterator]();
console.log(it.next()); // { value: 1, done: false }
console.log(it.next()); // { value: 2, done: false }イテレータ自体は無限でもOK ですが、for...of などで使う場合は
必ずどこかで break する、回数を制限する、など制御が必要です。
ジェネレータ関数とは何か
function* と yield
ジェネレータ関数 は function* で宣言します。
function* gen() {
console.log("start");
yield 1;
console.log("between");
yield 2;
console.log("end");
}呼び出し:
const g = gen();
console.log(g.next()); // "start" -> { value: 1, done: false }
console.log(g.next()); // "between" -> { value: 2, done: false }
console.log(g.next()); // "end" -> { value: undefined, done: true }ポイント:
gen()を呼んだ瞬間には 中のコードはまだ実行されない- 「実行可能なジェネレータオブジェクト」を返すだけ
g.next()を呼ぶたびに、- 前回
yieldで止まっていたところから再開し、 - 次の
yieldまで進んで一旦止まり、 { value, done }を返す
- 前回
done: trueになったらそれ以上は進まない
つまりジェネレータは、「実行を一時停止・再開できる関数」+「イテレータを簡単に作るための仕組み」です。
ジェネレータは「イテレータかつイテラブル」
function* で作ったジェネレータオブジェクトは、イテレータであると同時に イテラブル でもあります。
function* gen() {
yield 1;
yield 2;
yield 3;
}
const g = gen();
console.log(typeof g[Symbol.iterator]); // "function"
console.log(g[Symbol.iterator]() === g); // trueg[Symbol.iterator]()はg自身を返す- つまり
for...ofにそのまま渡せる
for (const v of gen()) {
console.log(v); // 1 2 3
}ジェネレータでイテラブルを簡単に書く
カウンターイテラブルをジェネレータで書き直す
さっきのカウンターイテラブル:
const counter = {
from: 1,
to: 5,
[Symbol.iterator]() {
let current = this.from;
const last = this.to;
return {
next() {
if (current <= last) {
return { value: current++, done: false };
} else {
return { value: undefined, done: true };
}
}
};
}
};これをジェネレータで書くとこうなります:
const counter2 = {
from: 1,
to: 5,
*[Symbol.iterator]() { // ← メソッドでも * が付けられる
for (let n = this.from; n <= this.to; n++) {
yield n;
}
}
};
for (const n of counter2) {
console.log(n); // 1 2 3 4 5
}*[Symbol.iterator]()の中でyieldしているだけnext()オブジェクトを手書きしていた部分が、ほぼ自動で処理される のがポイントです。
ジェネレータで無限シーケンスを定義
function* naturals() {
let n = 1;
while (true) {
yield n++;
}
}
const it = naturals();
console.log(it.next()); // { value: 1, done: false }
console.log(it.next()); // { value: 2, done: false }
console.log(it.next()); // { value: 3, done: false }while (true)でも、実際にnextを呼ばない限り進まない- 「必要になったときにだけ値を計算する(遅延評価)」という性質がわかりやすく現れます
yield と next() の値のやりとり
yield の評価結果
function* gen() {
const x = yield 1;
console.log("x =", x);
const y = yield 2;
console.log("y =", y);
}呼び出し:
const g = gen();
console.log(g.next()); // { value: 1, done: false }
console.log(g.next(10)); // 「10」が一つ前のyield式の結果になる
// → x = 10
// { value: 2, done: false }
console.log(g.next(20)); // → y = 20
// { value: undefined, done: true }yieldで「外側に渡す値」はvalue- 「次の
next(arg)のargが 前回のyield式の評価結果 になる
実務ではここまで凝ったことはあまりしませんが、
- 「双方向通信が可能」
- 「イテレータ+コルーチン的な動き」
をするのがジェネレータの強みのひとつです。
return と throw
- ジェネレータ内で
return valueすると、その時点でdone: trueになり、valueが返る - ただし
for...ofなど多くの「使用する側」は、done: trueのときのvalueは無視する
function* gen() {
yield 1;
return 999;
}
const g = gen();
console.log(g.next()); // { value: 1, done: false }
console.log(g.next()); // { value: 999, done: true }
console.log(g.next()); // { value: undefined, done: true }外から g.throw(error) を呼んでエラーを投げ込むこともできますが、
用途が限られるので、「そういうこともできる」程度の理解で十分です。
ジェネレータの代表的な用途
ここまでの知識をどう使うか、代表的なパターンだけイメージしておきます。
- カスタム反復処理
- 配列やツリー構造(DOM・自作データ)を「特定の順番で」たどりたいとき
- 例:木構造を前順・後順・幅優先など好みの順で走査
- 大きなデータの「遅延評価」
- 巨大配列を全部メモリに持たず、「必要なぶんだけ順次読み込んで処理」
- ログファイルを1行ずつジェネレータで読む、など(Node + ストリームと組み合わせ)
- 処理パイプラインの実装
- map/filter 的な処理を、配列に変換せずジェネレータ同士つなげる
- パフォーマンス重視のライブラリではよく使われるパターン
- テスト用・疑似データ生成
- 無限シーケンスや擬似乱数列を生成して、テスト用データを簡単に作る
「書かないとメリットが分かりにくい」機能ですが、イテレータの基礎理解としては重要 です。
非同期イテレータとは何か
これまで見てきた(同期)イテレータは、
obj[Symbol.iterator]()がイテレータを返す- そのイテレータの
next()が 同期的に{ value, done }を返す
というプロトコルでした。
非同期イテレータは、その「非同期版」です。
非同期イテレータのプロトコル
非同期イテレータは、次の2つを満たすオブジェクトです。
[Symbol.asyncIterator]()メソッドを持つconst asyncIterable = { [Symbol.asyncIterator]() { return {/* ここでイテレータを返す */} } };- そのイテレータの
next()が Promise を返すconst asyncIterator = asyncIterable[Symbol.asyncIterator](); // next() の戻り値は Promise<{ value, done }> const p = asyncIterator.next(); p.then(({ value, done }) => { // ここに到達するタイミングは非同期 });
まとめると、「[Symbol.asyncIterator]() で取り出せて、next() が Promise を返すもの」が非同期イテレータです。
async ジェネレータ(async function*)
非同期イテレータを素手で実装するのは面倒なので、
同期ジェネレータのときと同じく 専用の構文 が用意されています。
// 非同期ジェネレータ関数
async function* genNumbers() {
let i = 0;
while (true) {
await new Promise(r => setTimeout(r, 1000)); // 1秒待つ(非同期)
yield i++; // 1秒ごとに値を生成
}
}この genNumbers() を呼び出すと、
const it = genNumbers(); // 非同期イテレータを返す
// it[Symbol.asyncIterator]() === it (イテレータ自身)
// it.next() は Promise<{ value, done }>のような 非同期イテレータ になります。
同期ジェネレータとの違い
- 同期ジェネレータ:
function*next()は 即座に{ value, done }を返す
- 非同期ジェネレータ:
async function*next()は Promise で{ value, done }を返す- 中で
awaitが使える yieldで値を「非同期に」順番に流せる
イメージとしては、「非同期処理を挟みつつ、1つずつ値を流していくストリーム」を実現するための構文、という感じです。
for await…of の基本
for...of が「同期イテレータ用のループ」だったのに対し、for await...of は「非同期イテレータ用のループ」 です。
基本形
for await (const value of asyncIterable) {
// asyncIterable からの値を、順番に非同期で受け取る
}ここで asyncIterable は:
Symbol.asyncIteratorを持つ非同期イテラブル
または- (多くの環境では)
Symbol.iteratorを持つ「普通のイテラブル」- この場合、各値がPromiseなら
awaitしてくれる
- この場合、各値がPromiseなら
async ジェネレータとの組み合わせ
さっきの genNumbers() を for await...of で回す例:
async function main() {
const it = genNumbers();
for await (const n of it) {
console.log(n);
if (n >= 3) break;
}
}
main();動作イメージ:
genNumbers()を呼ぶ → 非同期イテレータitを得るfor awaitは内部でit[Symbol.asyncIterator]()を呼び- 毎回
await it.next()して{ value, done }を取り出す
yieldされた値がnに入り、ループ本体が実行されるdone: trueになったらループ終了
同期版との違いは:
- 各ステップが
await前提になっているので
「値が準備でき次第」ループが続く、という非同期フローを自然に書ける点です。
現実的なユースケース
API を順番に叩くストリーム風の処理
// 複数ページのAPIを順に叩く非同期ジェネレータ
async function* fetchPages(startPage = 1) {
let page = startPage;
while (true) {
const res = await fetch(`https://example.com/items?page=${page}`);
if (!res.ok) {
throw new Error("HTTP error " + res.status);
}
const data = await res.json();
if (data.items.length === 0) {
return; // ここで done: true
}
yield data.items; // 1ページ分の結果を yield
page++;
}
}
async function main() {
try {
for await (const items of fetchPages()) {
console.log("ページを取得:", items.length, "件");
// 必要ならここで break もできる
if (/* もう十分 */ false) {
break;
}
}
} catch (e) {
console.error("取得中にエラー:", e);
}
}
main();ここでのポイント:
fetchPages()側は 「次のページを取ってきたら yield」 という自然な書き方- 呼び出し側は 「for await…of で順番に受け取る」 だけ
Node.js のストリーム風の使い方
Node.jsの Readable ストリーム(最近のバージョン)にはSymbol.asyncIterator もあります。
import { createReadStream } from "node:fs";
async function main() {
const stream = createReadStream("big.txt", { encoding: "utf8" });
for await (const chunk of stream) {
console.log("チャンク:", chunk.length);
}
console.log("読み込み完了");
}for await...ofでストリームからのデータをチャンクごとに非同期で受け取る- 「読み終わるまで待つ」のではなく、届いた分から順に処理できる
エラー処理と終了処理
try/catch と組み合わせ
for await...of は await と同じく、内部で起きた reject を throw に変換してくれるので、
async function main() {
try {
for await (const v of asyncIterable) {
console.log(v);
}
} catch (e) {
console.error("ループ中にエラー:", e);
}
}のように try/catch でまとめてハンドリングできます。
途中で break / return したとき
for await...of から break・return・throw で抜けるとき、
- ジェネレータ側に
return()/throw()が呼ばれて - 必要ならクリーンアップ処理(
finally的なもの)を入れられます
たとえば、非同期ジェネレータ内部に try/finally を書いておくと:
async function* gen() {
try {
yield 1;
yield 2;
yield 3;
} finally {
console.log("クリーンアップ");
}
}
async function main() {
for await (const x of gen()) {
console.log(x);
break; // ここで抜ける
}
}
// 実行すると 1 → "クリーンアップ" といった出力になる
コメント