ES6で変わったリストの巡回とイテラブル/イテレーターのプロトコル
この記事では、JavaScript ES6で導入された新しいリスト巡回方式と、それを支えるイテラブル/イテレーターのプロトコルについて解説します。
1. 従来の方式と変化したES6でのリスト巡回
従来のES5におけるリスト巡回
ES5では、リストを巡回するために以下のように記述していました:
const list = [1, 2, 3];
for (var i = 0; i < list.length; i++) {
console.log(list[i]);
}
-
list.length
というプロパティを利用して、繰り返し条件を設定します。 - インデックス
i
を使ってリストの要素にアクセスします。
文字列の場合も同様です:
const str = 'abc';
for (var i = 0; i < str.length; i++) {
console.log(str[i]);
}
ES6のリスト巡回
ES6では、for...of
文を使って以下のようにリストを巡回できます:
const list = [1, 2, 3];
for (const a of list) {
console.log(a);
}
- 具体ていな命令後を書く複雑なインデックスベースの巡回ではなく、
for...of
を通してより宣言的な方法でリストを巡回します。 - これは単に文法的な便利さを提供するだけでなく、イテラブル/イテレーターのプロトコルという重要な概念と結びついています。
2. イテラブル/イテレーターのプロトコル
Array, Set, Mapなど、ES6で提供する様々な組み込みオブジェクトは全てイテラブル/イテレーターのプロトコルを従います。
イテラブルとイテレーターの定義
- イテラブル(Iterable):
Symbol.iterator
メソッドを持ち、それを呼び出すとイテレーターを返すオブジェクト。 - イテレーター(Iterator):
next()
メソッドを持ち、呼び出すと{ value, done }
オブジェクトを返すオブジェクト。 - イテラブル/イテレーターのプロトコル: イテラブルとイテレーターが従う規約。この規約に従うオブジェクトは、
for...of
文やスプレッド構文...
などの高度な文法と共に動作することができます。
Array/Set/Map巡回の例
1. Array
const arr = [1, 2, 3];
for (const a of arr) {
console.log(a);
}
// 結果: 1, 2, 3
console.log(arr[0]); // 1
console.log(arr[1]); // 2
console.log(arr[2]); // 3
Arrayは数値インデックスを介して要素にアクセスできますが、for...of
文はこれとは異なる方法で動作します。つまり、for...of
文の内部は、ES5のfor文の反復処理のようにインデックスを介したアクセスによって反復処理を行うわけではありません。その例として、以下のSetを見てみましょう。
2. Set
const set = new Set([1, 2, 3]);
for (const a of set) {
console.log(a);
}
// 結果: 1, 2, 3
console.log(set[0]); // undefined
Setはインデックスを使ったアクセスが不可能ですが、内部的にfor...of
文を通じて反復処理することができます。つまり、for...of
文の内部は、私たちが知っているインデックスを使ったアクセスでは動作しないということです。
3. Map
const map = new Map([
['a', 1],
['b', 2],
['c', 3],
]);
for (const a of map) {
console.log(a);
}
// 結果: ['a', 1], ['b', 2], ['c', 3]
console.log(map[0]); // undefined
Mapもまた数値インデックスでアクセスすることはできませんが、for...of
文を通じて[key, value]
ペアを反復処理することができます。
3. Symbol.iteratorと巡回の抽象化
イテラブル/イテレーターのプロトコルは内部的にSymbol.iterator
を通じて動作します。これにより、for...of
文やスプレッド構文などのさまざまな巡回機能が抽象化されています。
Symbol.iterator
Symbol.iterator
はイテラブルオブジェクトに内蔵されるメソッドで、イテレーターを返します。これを直接呼び出して確認できます:
const arr = [1, 2, 3];
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()
メソッドは巡回状態を維持し、巡回が終了すると{ done: true }
を返します。ある瞬間、value
がなくなり、done
がtrue
になります。 -
for...of
文は内部的に、iterator,next()
のvalue
に該当する値をa
に入れて出力し、done
がtrue
になったらfor
文から抜けるようになっています。
Symbol.iteratorを削除すると巡回失敗
Symbol.iterator
を削除すると、巡回が不可能になります:
const arr = [1, 2, 3];
arr[Symbol.iterator] = null;
for (const a of arr) {
console.log(a);
}
// エラー: arr is not iterable
- Arrayがイテラブルである理由は、イテレーターを返す
Symbol.iterator
を持っているからです。 -
arr[Symbol.iterator]
の値をnullに設定すると、arr
はもはやイテラブルではなくなります。そのため、反復処理は失敗し、エラーが発生します。
巡回中間状態の確認
イテレーターは状態を維持するため、途中でnext()
を呼び出すとその後の状態に進みます:
const arr = [1, 2, 3];
const iter = arr[Symbol.iterator]();
iter.next(); // { value: 1, done: false }
for (const a of iter) {
console.log(a);
}
// 結果: 2, 3
上の例のように実行すると、出力が2
から実行されろことが確認できます。
Mapのkeys(), values(), entries()メソッド
Mapの場合をもっと詳しくみましょう。
Mapオブジェクトは、それぞれの役割に応じたイテレーターを返す3つのメソッドを提供します:
- keys(): キー(key)を巡回するイテレーターを返します。
- values(): 値(value)を巡回するイテレーターを返します。
- entries():
[key, value]
ペアを巡回するイテレーターを返します。
const map = new Map([
['a', 1],
['b', 2],
['c', 3],
]);
const keysIter = map.keys();
const valuesIter = map.values();
const entriesIter = map.entries();
console.log(keysIter.next()); // { value: 'a', done: false }
console.log(valuesIter.next()); // { value: 1, done: false }
console.log(entriesIter.next()); // { value: ['a', 1], done: false }
- それぞれのメソッドは独立したイテレーターを返し、互いに干渉しません。
- 私たちはMapオブジェクトが提供するメソッドを通じて、キー、値、エントリーにアクセスすることができるのです。
Discussion
key / value のペアなのに 数値インデックスでアクセスできないのは プレーンオブジェクトも同じでは……?
プレーンオブジェクト版