👏

ES6で変わったリストの巡回とイテラブル/イテレーターのプロトコル

2025/01/08に公開1

この記事では、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がなくなり、donetrueになります。
  • for...of文は内部的に、iterator,next()valueに該当する値をaに入れて出力し、donetrueになったら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

上の例のように実行すると、出力がから実行されろことが確認できます。

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オブジェクトが提供するメソッドを通じて、キー、値、エントリーにアクセスすることができるのです。

参考資料: JavaScript ES6+のための関数型プログラミングとJavaScript ES6+

Discussion

junerjuner

Mapもまた数値インデックスでアクセスすることはできませんが

key / value のペアなのに 数値インデックスでアクセスできないのは プレーンオブジェクトも同じでは……?

const map = new Map([
    ['a', 1],
    ['b', 2],
    ['c', 3],
]);

for (const key of map.keys()) {
    console.log(map.get(key));
}
// 結果: 1, 2, 3

console.log(map.get(0)); // undefined

プレーンオブジェクト版

const map = {
    'a': 1,
    'b': 2,
    'c': 3,
};

for (const key of Object.keys(map)) {
    console.log(map[key]);
}
// 結果: 1, 2, 3

console.log(map[0]); // undefined