反復処理プロトコル

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

プロトコルは 2 つあります。反復可能プロトコル反復子プロトコルです。

反復可能 (iterable) プロトコル

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

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

プロパティ
[Symbol.iterator] 反復子プロトコルに準拠するオブジェクトを返す、引数なしの関数。

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

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

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

反復子 (iterator) プロトコル

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

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

プロパティ
next()

引数が 0 個または 1 個の関数で、少なくとも以下の 2 つのプロパティを持つオブジェクトを返します。 next() メソッドが1つの引数で呼び出された場合、その引数は呼び出された next() メソッドで利用可能です。

done (boolean)

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

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

value
反復子によって返される任意の JavaScript 値。 donetrue のときは省略することができます。

next() メソッドは常に donevalue などの適切なプロパティを持つオブジェクトを返します。オブジェクトでない値が返された場合 (例えば falseundefined)、 TypeError ("iterator.next() returned a non-object value") が発生します。

メモ: 特定のオブジェクトが反復子プロトコルを実装しているかどうかを反射的に知ることはできません。しかし、反復子プロトコルと反復可能プロトコルの両方を満たすオブジェクトを作成するのは簡単です(以下の例のように)。

そうすることで、反復可能オブジェクトを期待するさまざまな構文で反復子を使用できます。したがって、反復子プロトコルを実装するには反復可能プロトコルも実装しないと、ほとんど役に立ちません。

// 反復子と反復可能の両プロトコルを満たす
const myIterator = {
    next: function() {
        // ...
    },
    [Symbol.iterator]: function() { return this; }
};

しかし、可能であれば iterable[Symbol.iterator]Set.prototype[@@iterator]() のように、常に先頭から始まる異なる反復子を返した方がよいでしょう。

反復処理プロトコルの使用例

String は組み込み反復可能オブジェクトの一例です。

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

String既定の反復子は文字列のコードポイントを 1 つずつ返します。

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 }

一部の組み込みコンストラクター — 例えばスプレッド構文 — は、まったく同じ反復処理プロトコルを使用しています。

console.log([...someString]); // ["h", "i"]

自身の @@iterator を提供することによって反復動作を再定義できます。

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

someString[Symbol.iterator] = function () {
  return {
    // これは反復子オブジェクトであり、単一の要素 (文字列 "bye") を返します。
    next: function () {
      return this._first ? {
        value: 'bye',
        done: (this._first = false)
      } : {
        done: true
      }
    },
    _first: true
  };
};

@@iterator を再定義することによって、反復処理プロトコルを使用する組み込みコンストラクターの動作にどれほど影響を与えるか注意してください。

console.log([...someString]); // ["bye"]
console.log(someString + ''); // "hi"

反復可能プロトコルの例

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

String, Array, TypedArray, Map, Set は、すべての組み込み反復可能オブジェクトです。というのも、それらすべてのプロトタイプオブジェクトは @@iterator メソッドをもつからです。

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

下記のように反復可能オブジェクトを生成できます。

const myIterable = {};
myIterable[Symbol.iterator] = function* () {
    yield 1;
    yield 2;
    yield 3;
};
console.log([...myIterable]); // [1, 2, 3]

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

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

new Map([[1, 'a'], [2, 'b'], [3, 'c']]).get(2); // "b"

const myObj = {};

new WeakMap([
    [{}, 'a'],
    [myObj, 'b'],
    [{}, 'c']
]).get(myObj);             // "b"

new Set([1, 2, 3]).has(3); // true
new Set('123').has('2');   // true

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

関連情報

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

いくつかの文や式は反復可能オブジェクトを期待します。例えば、 for...of ループ、スプレッド演算子yield*分割代入 などです。

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"

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

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

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

const nonWellFormedIterable = {};
nonWellFormedIterable[Symbol.iterator] = () => 1;
[...nonWellFormedIterable]; // TypeError: [] is not a function

反復子の例

簡単な反復子

function makeIterator(array) {
  let nextIndex = 0
  return {
    next: function() {
      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

無限の反復子

function idMaker() {
  let index = 0;
  return {
    next: function() {
      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'
// ...

ジェネレーターで

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'
// ...

ES2015 クラスで

class SimpleClass {
  constructor(data) {
    this.data = data;
  }

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

    return {
      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'
}

ジェネレーターは反復子か反復可能か

ジェネレーターオブジェクト は、反復子でも反復可能でもあります。

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

console.log(typeof aGeneratorObject.next);
// "function", because it has a next method, so it's an iterator

console.log(typeof aGeneratorObject[Symbol.iterator]);
// "function", because it has an @@iterator method, so it's an iterable

console.log(aGeneratorObject[Symbol.iterator]() === aGeneratorObject);
// true, because its @@iterator method returns itself (an iterator), so it's an well-formed iterable

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

console.log(Symbol.iterator in aGeneratorObject)
// true, because @@iterator method is a property of aGeneratorObject

関連情報