反復処理プロトコル

反復処理プロトコルは、新しい組み込みオブジェクトや構文ではなくプロトコルです。これらのプロトコルは以下のような単純な約束事によって、すべてのオブジェクトで実装することができます。

プロトコルは 2 つあります。反復可能プロトコルイテレータープロトコルです。

反復可能プロトコル

反復可能プロトコル (The iterable protocol) によって、 JavaScript のオブジェクトは反復動作を定義またはカスタマイズすることができます。例えば、 for...of 構造の中でどの値がループに使われるかです。一部の組み込み型は既定の反復動作を持つ組み込み反復可能オブジェクトで、これには ArrayMap がありますが、他の型 (Object など) はそうではありません。

反復可能であるために、オブジェクトは @@iterator メソッドを実装する必要があります。これはつまり、オブジェクト(または、プロトタイプチェーン上のオブジェクトの一つ)が Symbol.iterator 定数にて利用できる @@iterator キーのプロパティを持つ必要があります。

[Symbol.iterator]

イテレータープロトコルに準拠するオブジェクトを返す、引数なしの関数。

for...of ループの始まりのように)オブジェクトが反復される必要があるときはいつでも、その @@iterator メソッドが引数なしで呼ばれます。そして、返されるイテレーターは、反復される値を取得するために使用されます。

なお、この引数なしの関数が呼び出されると、反復可能オブジェクト上のメソッドとして呼び出されます。従って関数の中では、 this キーワードを反復可能オブジェクトのプロパティにアクセスするために使用して、反復の間に何を提供するかを決めることができます。

この関数は普通の関数、またはジェネレーター関数にすることができ、そのため呼び出されると、イテレーターオブジェクトが返されます。このジェネレーター関数の中では yield を使用してそれぞれの項目を提供することができます。

イテレータープロトコル

イテレーター(反復子)プロトコル (The iterator protocol) は、値の並び(有限でも無限でも)を生成するための標準的な方法と、すべての値が生成された場合の返値を定義します。

以下の意味で next() メソッドを実装していれば、オブジェクトはイテレーターになります。

next()

0 個または 1 個の引数を受け入れ、IteratorResult インターフェイス(下記参照)に適合したオブジェクトを返す関数です。組み込み言語機能(for...of など)がイテレーターを使用しているときに、オブジェクト以外の値(falseundefined など)を返した場合、TypeError ("iterator.next() returned a non-object value") が発生します。

イテレータープロトコルのすべてのメソッド(next()return()throw())は、IteratorResult インターフェイスを実装したオブジェクトを返すことが期待されています。このオブジェクトは以下のプロパティを持つ必要があります。

done 省略可

イテレーターがシーケンス内の次の値を生成できるとき false の値になります。(これは done プロパティが指定されていない場合も同じです。)

イテレーターが反復シーケンスを終了した場合、true の値になります。この場合、value は任意でイテレーターの返値を指定します。

value 省略可

イテレーターによって返される任意の JavaScript 値。donetrue のときは省略することができます。

実際には、どちらのプロパティも厳密には必要ありません。どちらのプロパティも持たないオブジェクトを返した場合、それは事実上 { done: false, value: undefined } と同じです。

イテレーターが done: true の結果を返した場合、それ以降に next() を呼び出すと、同様に done: true を返すことが期待されますが、これは言語レベルでは強制されるものではありません。

next メソッドは、メソッド本体が利用できるようになる値を受け取ることができます。組み込みの言語機能では、値を渡すことはありません。ジェネレーターnext メソッドに渡される値は、対応する yield 式の値になる。

オプションとして、イテレーターは return(value)throw(exception) メソッドも実装することができます。これらのメソッドは、呼び出されると、呼び出し側がイテレーターの反復処理を終了し、(データベース接続を閉じるなどの)必要なクリーンアップを実行できることをイテレーターに指示します。

return(value) 省略可

0 または 1 つの引数を受け入れ、IteratorResult インターフェイスに適合したオブジェクトを返す関数で、通常 value は渡された value と等しく、donetrue と等しくなります。このメソッドを呼び出すと、呼び出し側がこれ以上 next() を呼び出さないことをイテレーターに指示し、任意のクリーンアップ処理を実行することができます。

throw(exception) 省略可

0 個または 1 個の引数を受け入れ、IteratorResult インターフェースに適合したオブジェクトを返す関数であり、通常 donetrue に等しくなります。このメソッドを呼び出すと、呼び出し側がエラー状態を検出したことをイテレーターに指示し、exception は通常 Error インスタンスとなります。

メモ: 特定のオブジェクトがイテレータープロトコルを実装しているかどうかを反射的に(つまり、実際に next() を呼び出して、返された結果を検証することなく)知ることは不可能です。

イテレーターはとても簡単に反復可能オブジェクトにすることができます。[@@iterator]() メソッドを実装して this を返すだけです。

js
// イテレーターと反復可能の両プロトコルを満たす
const myIterator = {
  next() {
    // ...
  },
  [Symbol.iterator]() {
    return this;
  },
};

このようなオブジェクトは反復可能イテレーターと呼ばれます。このように実装することで、反復可能オブジェクトを期待する様々な構文でイテレーターが利用できるようになります。したがって、反復可能プロトコルを実装せずにイテレータープロトコルを実装することは、ほとんど有益ではありません。(実際、ほとんどすべての構文と API はイテレーターではなく反復可能を期待しています。)ジェネレーターオブジェクトがその例です。

js
const aGeneratorObject = (function* () {
  yield 1;
  yield 2;
  yield 3;
})();

console.log(typeof aGeneratorObject.next);
// "function" — (正しい結果を返す)next メソッドを持っているのでイテレーター

console.log(typeof aGeneratorObject[Symbol.iterator]);
// "function" — (正しいイテレーターを返す)@@iterator メソッドを持っているので反復可能オブジェクト

console.log(aGeneratorObject[Symbol.iterator]() === aGeneratorObject);
// true — @@iterator メソッドは自分自身(イテレータ)を返すので、反復可能なイテレーター

組み込みのイテレーターはすべて Iterator.prototype を継承しており、 [@@iterator]() メソッドは this を返すものとして実装されているので、組み込みイテレーターも反復可能なものとなっています。

しかし、可能であれば iterable[Symbol.iterator]Set.prototype[@@iterator]() が行うように、常に先頭から始まる別のイテレーターを返すほうが良いでしょう。

非同期イテレーターと非同期反復可能プロトコル

非同期の反復処理に用いるプロトコルには、非同期イテレーター非同期反復可能オブジェクトという名前の別の組があります。これらは反復可能オブジェクトとイテレータープロトコルに比べて非常によく似たインターフェイスを持っていますが、イテレーターメソッドの呼び出しからの返値がそれぞれプロミスでラップされている点が異なります。

オブジェクトが非同期イテレータープロトコルを実装するのは、以下のメソッドを実装している場合です。

[Symbol.asyncIterator]

非同期イテレータープロトコルに準拠し、オブジェクトを返す引数ゼロの関数です。

オブジェクトが非同期イテレータープロトコルを実装するのは、以下のメソッドを実装している場合です。

next()

0 個または 1 個の引数を受け入れ、プロミスを返す関数です。プロミスは IteratorResult インターフェイスに準拠したオブジェクトに履行され、プロパティは同期イテレーターのものと同じ意味づけを持ちます。

return(value) 省略可

0 個または 1 個の引数を受け入れ、プロミスを返す関数です。プロミスは IteratorResult インターフェイスに準拠したオブジェクトに履行され、プロパティは同期イテレーターのものと同じ意味づけを持ちます。

throw(exception) 省略可

0 個または 1 個の引数を受け入れ、プロミスを返す関数です。プロミスは IteratorResult インターフェイスに準拠したオブジェクトに履行され、プロパティは同期イテレーターのものと同じ意味づけを持ちます。

言語と反復処理プロトコルの対話

この言語では、反復可能オブジェクトとイテレーターを生成または使用する API を規定しています。

組み込み反復可能オブジェクト

StringArrayTypedArrayMapSetSegmentsIntl.Segmenter.prototype.segment() で返される)は、すべて組み込み反復可能オブジェクトです。それらの prototype オブジェクトはそれぞれ @@iterator メソッドを実装しています。また、arguments オブジェクトや NodeList などの一部の DOM コレクション型も反復可能です。 ReadableStreamは、執筆時点では唯一の組み込み非同期反復可能オブジェクトです。

ジェネレータ関数は、反復可能なイテレーターであるジェネレータオブジェクトを返します。非同期ジェネレータ関数は、非同期反復可能なイテレータである非同期 ジェネレーターオブジェクトを返します。

組み込み反復可能オブジェクトから返されるイテレーターは、実際にはすべて共通のクラス Iterator (現在は未公開)を継承しており、前述の [Symbol.iterator]() { return this; } メソッドを実装しているので、すべて反復可能イテレーターとなっています。将来的には、これらの組み込みイテレーターは、イテレータープロトコルが要求する next() メソッドに加えて、追加のヘルパーメソッドを持つことができます。イテレーターのプロトタイプチェーンは、グラフィカルコンソールにログ出力することで検査することができます。

console.log([][Symbol.iterator]());

Array Iterator {}
  [[Prototype]]: Array Iterator     ==> 配列のイテレーターに共通するプロトタイプ
    next: ƒ next()
    Symbol(Symbol.toStringTag): "Array Iterator"
    [[Prototype]]: Object           ==> すべての組み込みイテレーターに共通するプロトタイプ
      Symbol(Symbol.iterator): ƒ [Symbol.iterator]()
      [[Prototype]]: Object         ==> これは Object.prototype

反復可能オブジェクトを受け入れる組み込み API

反復可能オブジェクトを受け入れる API はたくさんあります。以下はその例です。

js
const myObj = {};

new WeakSet(
  (function* () {
    yield {};
    yield myObj;
    yield {};
  })(),
).has(myObj); // true

反復可能オブジェクトを期待する構文

一部の文や式は反復可能オブジェクトを期待します。例えば、 for...of ループ、配列と引数のスプレッドyield*配列の分割代入などです。

js
for (const value of ["a", "b", "c"]) {
  console.log(value);
}
// "a"
// "b"
// "c"

console.log([..."abc"]); // ["a", "b", "c"]

function* gen() {
  yield* ["a", "b", "c"];
}

console.log(gen().next()); // { value: "a", done: false }

[a, b, c] = new Set(["a", "b", "c"]);
console.log(a); // "a"

組み込み API がイテレーターを反復処理していて、最後の結果の donefalse (イテレーターがさらに値を生成できる状態)だが、それ以上の値は必要ない場合、return メソッドが存在すれば、それが呼び出されますこれは例えば、for...of ループの中で breakreturn に遭遇した場合や、配列の分割代入ですべての識別子が既に結合されている場合などに発生します。

js
const obj = {
  [Symbol.iterator]() {
    let i = 0;
    return {
      next() {
        i++;
        console.log("Returning", i);
        if (i === 3) return { done: true, value: i };
        return { done: false, value: i };
      },
      return() {
        console.log("Closing");
        return { done: true };
      },
    };
  },
};

const [b] = obj;
// Returning 1
// Closing

const [a, b, c] = obj;
// Returning 1
// Returning 2
// Returning 3
// Already reached the end (the last call returned `done: true`),
// so `return` is not called

for (const b of obj) {
  break;
}
// Returning 1
// Closing

非同期反復可能オブジェクトと対話する唯一の方法は、for await...of ループと、非同期ジェネレーター関数での yield*(ただし同期ジェネレーター関数ではありません)です。非同期反復可能オブジェクトであるが、同期反復可能ではない(つまり [@@asyncIterator]() はあるが [@@iterator]() はない)ものに for...of や配列の展開などを使用すると、TypeError: x is not iterable 例外が発生します。

非正規形反復可能オブジェクト

反復可能オブジェクトの @@iterator メソッドがイテレーターオブジェクトを返さない場合、それは非正規形反復可能オブジェクトと見なされます。

これを使用すると、ランタイムエラーやバグの挙動をもたらす可能性があります。

js
const nonWellFormedIterable = {};
nonWellFormedIterable[Symbol.iterator] = () => 1;
[...nonWellFormedIterable]; // TypeError: [Symbol.iterator]() returned a non-object value

ユーザー定義の反復可能オブジェクト

次のようにして、独自の反復可能オブジェクトを作ることができます。

js
const myIterable = {
  *[Symbol.iterator]() {
    yield 1;
    yield 2;
    yield 3;
  },
};

console.log([...myIterable]); // [1, 2, 3]

単純なイテレーター

イテレーターはもともとステートフルなものです。ジェネレーター関数として定義していない場合(上の例が示すように)、クロージャで状態をカプセル化したいと思うことでしょう。

js
function makeIterator(array) {
  let nextIndex = 0;
  return {
    next() {
      return nextIndex < array.length
        ? {
            value: array[nextIndex++],
            done: false,
          }
        : {
            done: true,
          };
    },
  };
}

const it = makeIterator(["yo", "ya"]);

console.log(it.next().value); // 'yo'
console.log(it.next().value); // 'ya'
console.log(it.next().done); // true

無限のイテレーター

js
function idMaker() {
  let index = 0;
  return {
    next() {
      return {
        value: index++,
        done: false,
      };
    },
  };
}

const it = idMaker();

console.log(it.next().value); // 0
console.log(it.next().value); // 1
console.log(it.next().value); // 2
// ...

ジェネレーターで反復可能プロトコルを定義

js
function* makeSimpleGenerator(array) {
  let nextIndex = 0;
  while (nextIndex < array.length) {
    yield array[nextIndex++];
  }
}

const gen = makeSimpleGenerator(["yo", "ya"]);

console.log(gen.next().value); // 'yo'
console.log(gen.next().value); // 'ya'
console.log(gen.next().done); // true

function* idMaker() {
  let index = 0;
  while (true) {
    yield index++;
  }
}

const it = idMaker();

console.log(it.next().value); // 0
console.log(it.next().value); // 1
console.log(it.next().value); // 2
// ...

クラスで反復可能プロトコルを定義

状態のカプセル化は、プライベートプロパティでも行うことができます。

js
class SimpleClass {
  #data;

  constructor(data) {
    this.#data = data;
  }

  [Symbol.iterator]() {
    // それぞれのイテレーターに新しいインデックスを使用します。これにより
    // break の使用や同じ反復可能オブジェクトに対する入れ子ループなど、自明でない用途で、
    // 反復可能オブジェクトに対する複数の反復処理を安全に行うことができます。
    let index = 0;

    return {
      // 注: アロー関数を使用すると、`next()` の代わりに `[@@iterator]()` の方を `this` が指すようになります。
      next: () => {
        if (index < this.#data.length) {
          return { value: this.#data[index++], done: false };
        } else {
          return { done: true };
        }
      },
    };
  }
}

const simple = new SimpleClass([1, 2, 3, 4, 5]);

for (const val of simple) {
  console.log(val); // 1 2 3 4 5
}

組み込み反復可能オブジェクトのオーバーライド

例えば、String は、組み込みのイテレーターオブジェクトです。

js
const someString = "hi";
console.log(typeof someString[Symbol.iterator]); // "function"

String既定のイテレーターは、文字列のコードポイントを 1 つずつ返します。

js
const iterator = someString[Symbol.iterator]();
console.log(`${iterator}`); // "[object String Iterator]"

console.log(iterator.next()); // { value: "h", done: false }
console.log(iterator.next()); // { value: "i", done: false }
console.log(iterator.next()); // { value: undefined, done: true }

自分自身で @@iterator を用意することで、反復処理の動作を再定義することができます。

js
// 自動ボックス化を避けるため、文字列オブジェクトを明示的に構築する必要があります。
const someString = new String("hi");

someString[Symbol.iterator] = function () {
  return {
    // this is the iterator object, returning a single element (the string "bye")
    next() {
      return this._first
        ? { value: "bye", done: (this._first = false) }
        : { done: true };
    },
    _first: true,
  };
};

@@iterator の再定義が、反復処理を使用する組み込みの構成要素の動作にどのような影響を与えるかに注目してください。

js
console.log([...someString]); // ["bye"]
console.log(`${someString}`); // "hi"

仕様書

Specification
ECMAScript Language Specification
# sec-iteration

関連情報