JavaScript中級 #3 イテレータとジェネレータ
#2 非同期入門に続いて、今回はJavaScriptのもう一つの中核ツール — イテレータとジェネレータ。
for...of はどう動作するのか?
#
基礎の#3 制御フローで for...of で配列を巡回しました。
for (const x of [1, 2, 3]) {
console.log(x);
}これは配列だけで動作するのではありません。JavaScriptはオブジェクトが一定の約束(イテラブルプロトコル)に従えば、すべて for...of で巡回できるように作っておきました。
for (const ch of 'hello') { // 文字列
console.log(ch);
}
for (const [k, v] of new Map([['a', 1], ['b', 2]])) { // Map
console.log(k, v);
}
for (const x of new Set([1, 2, 3])) { // Set
console.log(x);
}配列、文字列、Map、Set、そして NodeList(document.querySelectorAll)などはすでにイテラブルです。すべて同じ約束に従います。
イテラブルプロトコル #
オブジェクトがイテラブルであるためには、次の一つを持っている必要があります。
Symbol.iteratorというキーに関数があること。 その関数を呼び出すとnext()を持ったイテレータを返すこと。next()は{ value, done }オブジェクトを返すこと。
直接作ってみると理解が早いです。
const range = {
from: 1,
to: 5,
[Symbol.iterator]() {
let current = this.from;
const last = this.to;
return {
next() {
if (current <= last) {
return { value: current++, done: false };
}
return { value: undefined, done: true };
},
};
},
};
for (const n of range) {
console.log(n);
}
// 1, 2, 3, 4, 5
Symbol.iterator が核心です。キーの位置に [...] が入っているのは基礎 #5の計算済みプロパティ構文です。Symbol.iterator という特別なシンボルをキーとして使います。
for...of は内部的にこの約束に従って next() を繰り返し呼び出しながら done が true になるまで値を取り出します。
Spread とデストラクチャリングもイテラブル #
for...of のほかに spread と配列デストラクチャリングもイテラブルプロトコルを使います。
const arr = [...range];
// [1, 2, 3, 4, 5]
const [first, second] = range;
// first = 1, second = 2
Math.max(...range); // 5
Array.from(range); // [1, 2, 3, 4, 5]
これが強力です — 一度イテラブルにすればJavaScriptのすべてのシーケンスツールをそのまま使えます。
ジェネレータ — イテラブルを短く作る構文 #
上で作った range は動きますが、イテラブルを作るコードが長く複雑です。JavaScriptには同じことを一度にしてくれる構文があります — ジェネレータ。
function* range(from, to) {
for (let n = from; n <= to; n++) {
yield n;
}
}
for (const n of range(1, 5)) {
console.log(n);
}
// 1, 2, 3, 4, 5
[...range(1, 5)]; // [1, 2, 3, 4, 5]
構文:
function*— アスタリスクが付いた関数 → ジェネレータyield— 一つの値を出して止まる- 再び呼ばれると
yieldの次の行から続く
ジェネレータ関数を呼び出すとイテラブルなオブジェクトが返されます。for...of がそのオブジェクトから値を一つずつ取り出します。
遅延シーケンス — 無限シーケンスも可能 #
ジェネレータの真の強みは必要な分だけ作る点です。無限シーケンスも表現可能です。
function* naturals() {
let n = 1;
while (true) {
yield n++;
}
}
const gen = naturals();
gen.next(); // { value: 1, done: false }
gen.next(); // { value: 2, done: false }
gen.next(); // { value: 3, done: false }
// ... 永遠に
while (true) が無限ループですが止まりません。next() を呼び出すたびに次の値だけを作ります。呼び出し時点でだけ計算 — これが遅延(lazy)の意味です。
無限シーケンスを切って使う #
function* take(iterable, n) {
let count = 0;
for (const x of iterable) {
if (count >= n) return;
yield x;
count++;
}
}
const first10 = [...take(naturals(), 10)];
// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
これが関数型ライブラリがよく提供する take 関数の正体です。
ジェネレータの他の活用 #
1) ツリー/グラフの巡回 #
function* walk(node) {
yield node.value;
for (const child of node.children ?? []) {
yield* walk(child); // 別のジェネレータに委譲
}
}
const tree = {
value: 'root',
children: [
{ value: 'a', children: [{ value: 'a1' }] },
{ value: 'b' },
],
};
for (const v of walk(tree)) {
console.log(v);
}
// root, a, a1, b
yield* は他のイテラブルの値をまるごと流し送る構文。再帰的な巡回をすっきりと表現できます。
2) 段階的に進む作業 #
function* steps() {
console.log('ステップ1 開始');
yield;
console.log('ステップ2 開始');
yield;
console.log('ステップ3 終了');
}
const s = steps();
s.next(); // ステップ1 開始
s.next(); // ステップ2 開始
s.next(); // ステップ3 終了
呼び出し側が明示的に next() を呼びながら段階を進めます。テストやシミュレーションの場面でしばしば活用されます。
非同期イテレータ — for await...of
#
非同期データを巡回するときに使う変形。ストリーミングデータ(ファイル、fetch レスポンスの chunk)を扱うときに出会います。
async function readChunks(response) {
for await (const chunk of response.body) {
console.log(chunk);
}
}詳しい使用は次の#6 fetch APIで扱います。
Symbol とは何か?
#
Symbol.iterator が突然登場しましたが、Symbol もJavaScriptのプリミティブ型の一つです。ユニークな識別子を作ってくれる値です。
const s1 = Symbol('id');
const s2 = Symbol('id');
s1 === s2; // false — 同じ説明でも別のシンボル
言語レベルのキー(Symbol.iterator、Symbol.asyncIterator など)は絶対に衝突しないようにするため Symbol で定義されています。一般的なコードでは頻繁に作ることはありませんが、ライブラリがメタデータをオブジェクトに付けるときに衝突なしで使う場面が時々あります。
まとめ #
この記事で整理した内容:
for...of/ spread / デストラクチャリングはイテラブルプロトコル上で動作- イテラブルになるには
Symbol.iteratorでnext()を持ったイテレータを返す - ジェネレータ(
function*、yield)でイテラブルを短く作る - 遅延シーケンス — 無限シーケンスも表現可能
yield*で他のイテラブルへ委譲for await...ofで非同期イテラブルを巡回- Symbol は衝突しないユニークな識別子
次の記事(#4 デストラクチャリングと spread/rest 詳細)では、基礎で軽く扱ったデストラクチャリングと spread をより深く — ネストパターン、引数デストラクチャリング、動的キーなどよく出会う実戦パターンを整理します。