[キャッチアップ] イテレーターとジェネレーター
イテレーターは配列など、連続するデータを順に取り出すための仕組みを持ったオブジェクトのことで、 以下の要素を持つオブジェクトを返す next()
メソッドを持つ。
-
value
: 反復中の次の値 -
done
: シーケンスの最後の値が既に消費されている場合true
となる値
配列(Array) 自体はイテレータではないが、 entries()
メソッドによって配列をイテレータに変換することも可能で、この場合 value
はインデックスと値のペアになる
const array = ["a", "b", "c"];
const arrayIterator = array.entries();
console.log(arrayIterator.next().value); // [0, "a"]
console.log(arrayIterator.next().value); // [1, "b"]
console.log(arrayIterator.next().value); // [2, "c"]
console.log(arrayIterator.next().done); // true
イテレータは for-of
と組み合わせることで、シーケンスが続く限り順に値を取り出すことができる。
イテレータ自体を知らなくても、配列を順に操作する場合はこのパターンはよく用いられる。
const array = ["a", "b", "c"];
const arrayIterator = array.entries();
for (const [key, value] of arrayIterator) {
console.log({ key, value }); // { key: 0, value: "a" }, { key: 1, value: "b" }, { key: 2, value: "c" }
}
配列の場合は初めにすべての要素を生成する必要があるが、イテレーターはその限りでなく、 next()
が呼び出されるたびに新たにデータを生成するようなオブジェクトであれば、操作した分だけ必要十分なデータ生成で済ませることが出来たり、終わりのない無限リストを生成することもできる。
next()
メソッドが value
done
を返すオブジェクトを自分で作ってみる。
任意の連続した値を生成するイテレータを次のように実装できる。
const makeRangeIterator = (start = 0, end = Infinity, step = 1) => {
let index = start;
return {
// for-of などによって反復可能なオブジェクトであることを示すフィールド
[Symbol.iterator]() {
return this;
},
// next メソッドを持つことで反復可能に
next() {
const value = index <= end ? index : undefined;
const done = index > end;
index += step;
// next メソッドは必ず value と done を返す
return {
value,
done,
};
},
};
};
const it = makeRangeIterator(0, 20, 5);
let current = it.next();
while (current.done === false) {
console.log(current.value); // 0, 5, 10, 15, 20
current = it.next();
}
前述の makeRangeIterator
みたいに、自前で next()
関数を持ったオブジェクトを生成するのは少々面倒であるため、ジェネレータ関数 (function*
) でそれを代替することができる。
function* makeRangeIterator(start = 0, end = Infinity, step = 1) {
let index = start;
while (index <= end) {
yield index;
index += step;
}
}
少々見慣れない構文ではあるが、上記関数は元の makeRangeIterator
と同じ挙動となり、具体的には以下のように動作する。
- 初回呼び出し時はコード実行自体はせずに、ジェネレーター(イテレーターのようなもの) を返す
- ジェネレーターの
next()
メソッドが呼ばれるたび、次のyield
が出現するまでコードを実行し、そのときの値をvalue
として返す -
yield
が出現せずにコードが終了した場合、done: true
を返す
ジェネレーターは for-of
などのイテラブルを受け取る構文とあわせて使用できる。
for (const iterator of makeRangeIterator(0, 20, 5)) {
console.log(iterator); // 0, 5, 10, 15, 20
}
あるオブジェクトがイテラブルであることを示す手段として、 Symbol.iterator
メソッドを定義する方法がある。
Synbol.iterator
メソッドが、next()
メソッドを持つオブジェクトを返す場合、それはイテラブルとして扱うことが出来る。
const makeMyIterable = (max = 100) => {
let i = 0;
return {
[Symbol.iterator]: () => {
return {
next: () => {
return {
value: i++,
done: i > max,
};
},
};
},
};
};
for (const iterator of makeMyIterable(5)) {
console.log(iterator); // 0, 1, 2, 3, 4
}
上記はジェネレータ関数で差し替えることも可能。
const makeMyIterable = (max = 100) => {
let i = 0;
return {
*[Symbol.iterator]() {
while (i < max) {
yield i++;
}
},
};
};
Array
を例にしたが、他にも String
Map
Set
なんかもイテラブルで、内部的には Symbol.iterator
メソッドが実装されている。
for (const iterator of [1, 2, 3]) {
console.log(iterator); // 1, 2, 3
}
for (const iterator of "abc") {
console.log(iterator); // a, b, c
}
for (const iterator of new Set([1, 2, 3])) {
console.log(iterator); // 1, 2, 3
}
for (const iterator of new Map([ [1, 2], [3, 4], [5, 6] ])) {
console.log(iterator); // [1, 2], [3, 4], [5, 6]
}
試してみるとこんな感じ
const array = [1, 2, 3];
console.log(array[Symbol.iterator]()); // [object Array Iterator]
MDN には他にも細かいキーワードや挙動の違い、内部の仕組みなどの話もあるけど、基本的にはこれぐらい。