JavaScript/TypeScript で配列などの繰り返し処理の仕様を学んでいると
Iterable、Iterator、Symbol.iterator、IterableIteratorといった、ややこしい用語がたくさん登場します。
字面の似ている用語がずらっと出てくるので、初見の人は割と混乱してとっつきにくい領域なんじゃないかなと思います。
そこで、繰り返し処理周りのこれらのオブジェクトや仕組みを、できるだけ話が単純になるように整理してみました。
Iterableって何?
Iterableとは、「繰り返し処理ができるオブジェクト」のことです。
かなりシンプルに言ってしまえば、for…ofループで使えるものはだいたいIterableです。
例えば、配列(Array)やSet、MapといったオブジェクトはすべてIterableです。
これらはfor…ofループを使って簡単に要素を一つずつ取り出すことができます。
const arr = [1, 2, 3];
for (const value of arr) {
console.log(value); // 1, 2, 3
}
ここでのポイントは、Arrayが反復可能(Iterable)だということです。
つまり、配列の要素を1つずつ順番に処理できるということです。
Iteratorって何?
Iteratorとは、オブジェクトの各要素を順番に取り出すための仕組みです。
Iteratorは、次のようなnext()メソッドを持ちます
// 返り値:
// - value: 現在の要素の値
// - done: 反復処理が終了したかどうか(falseで継続、trueで終了)
next(): { value: 要素, done: 処理が終了したかどうか }
下のコードのように、nextを呼び出す度に次の値を返します。
呼び出しているnext()メソッドは、「次の要素をください」と頼むメソッドです。
そしてdone: trueが返った場合は、一緒に返るvalueが最後の値、ということになります。
// ここの[Symbol.iterator]は下で説明します
const iterator = arr[Symbol.iterator]();
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { value: undefined, done: true }
このnext()メソッドを呼び出すたびに次の要素を順番に返し、
最後の要素まで達すると{ value: undefined, done: true }が返ってきます。
配列などIterableオブジェクトの繰り返し処理は、このIteratorが担っています。
[Symbol.iterator]って何?
上のコード例でも出てきた[Symbol.iterator]の説明です。
これは、配列などのIterableオブジェクトが持っている特殊なプロパティのキーであり、呼び出すことでIteratorを返します。
この[Symbol.iterator]を持っているオブジェクトは「私は反復処理ができますよ!」と宣言していることになります。
(すなわち[Symbol.iterator]を持っているオブジェクト=Iterableオブジェクトです)
IterableIteratorって何?
この用語は少しややこしく聞こえるかもしれませんが、実際は至ってシンプルです。
これは単純に、Iterableであり同時にIteratorでもあるオブジェクトのことを指します。
Iterableであり、IteratorでもあるからIterableIteratorという、安直で単純なネーミングです。
例えば、ジェネレーター関数が返すオブジェクトはこのIterableIteratorです。
ジェネレーターは、yieldというキーワードを使って、要素を一つずつ返す特殊な関数です。
呼び出すたびに次のyield値を返し、done: trueになるまで続けます。
function* myGenerator() {
yield 1;
yield 2;
yield 3;
}
const iterator = myGenerator();
for (const value of iterator) {
console.log(value); // 1, 2, 3
}
ここで、
「IterableIteratorは自身でnext()を持っている(繰り返し処理ができる)ので、わざわざ[Symbol.iterator]を持つ、つまり別のIteratorを呼び出す機能を持っている必要なんかないんじゃないのか?」
と感じる人もいるのかなと思います(自分はそう感じました)。
しかし実際には、
IterableIteratorも[Symbol.iterator]メソッドを持っている必要があります。
繰り返しになりますが、IterableIteratorは、次のような2つの性質を持っています。
- Iteratorである: next()メソッドを持ち、順番に値を返す機能を持っている
- Iterableである: for…ofループなどで反復処理を可能にするために、[Symbol.iterator]メソッドを持つ必要がある
そしてここがミソですが、
IterableIteratorの[Symbol.iterator]メソッドは、自分自身を返すように実装されています。
function* myGenerator() {
yield 1;
yield 2;
yield 3;
}
const iterableIterator = myGenerator();
// "function" (Iteratorとして`next`を持っている)
console.log(typeof iterableIterator.next);
// "function" (Iterableとして`[Symbol.iterator]`を持っている)
console.log(typeof iterableIterator[Symbol.iterator]);
// `Symbol.iterator`は自分自身を返す
console.log(iterableIterator[Symbol.iterator]() === iterableIterator); // true
こんな感じで、IterableIteratorの[Symbol.iterator]メソッドは、自分自身を返します。
これにより、for…ofなどの標準的な反復処理でも使えますし、next()を使って自分自身で次の要素を取得することも可能になるというわけです。
この2つの繰り返し処理ができるということが、IterableIteratorの特徴となります。
まとめ
Iterable:
– 繰り返し処理ができるオブジェクト(Array, Set, Map, …etc)
Iterator:
– オブジェクトの各要素を順番に取り出すための仕組み
– next()メソッドを持つ
[Symbol.iterator]:
– Iterableが持つプロパティキーであり、実行するとIteratorを返すメソッド
IterableIterator:
– Iterableであり、Iteratorでもあるオブジェクト
– [Symbol.iterator]は自分自身を返す
– 例: ジェネレーターなど
コメント