イテラブルとイテレータ
イテレータとは?
イテレータ(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 的な処理を、配列に変換せずジェネレータ同士つなげる
- パフォーマンス重視のライブラリではよく使われるパターン
- テスト用・疑似データ生成
- 無限シーケンスや擬似乱数列を生成して、テスト用データを簡単に作る
「書かないとメリットが分かりにくい」機能ですが、イテレータの基礎理解としては重要 です。
コメント