Closed10

[キャッチアップ] イテレーターとジェネレーター

shingo.sasakishingo.sasaki

イテレーターは配列など、連続するデータを順に取り出すための仕組みを持ったオブジェクトのことで、 以下の要素を持つオブジェクトを返す next() メソッドを持つ。

  • value: 反復中の次の値
  • done: シーケンスの最後の値が既に消費されている場合 true となる値

配列(Array) 自体はイテレータではないが、 entries() メソッドによって配列をイテレータに変換することも可能で、この場合 value はインデックスと値のペアになる

const array = ["a", "b", "c"];

const arrayIterator = array.entries();

console.log(arrayIterator.next().value); // [0, "a"]
console.log(arrayIterator.next().value); // [1, "b"]
console.log(arrayIterator.next().value); // [2, "c"]
console.log(arrayIterator.next().done); // true
shingo.sasakishingo.sasaki

イテレータは for-of と組み合わせることで、シーケンスが続く限り順に値を取り出すことができる。
イテレータ自体を知らなくても、配列を順に操作する場合はこのパターンはよく用いられる。

const array = ["a", "b", "c"];

const arrayIterator = array.entries();

for (const [key, value] of arrayIterator) {
  console.log({ key, value }); // { key: 0, value: "a" }, { key: 1, value: "b" }, { key: 2, value: "c" }
}
shingo.sasakishingo.sasaki

配列の場合は初めにすべての要素を生成する必要があるが、イテレーターはその限りでなく、 next() が呼び出されるたびに新たにデータを生成するようなオブジェクトであれば、操作した分だけ必要十分なデータ生成で済ませることが出来たり、終わりのない無限リストを生成することもできる。

shingo.sasakishingo.sasaki

next() メソッドが value done を返すオブジェクトを自分で作ってみる。
任意の連続した値を生成するイテレータを次のように実装できる。

const makeRangeIterator = (start = 0, end = Infinity, step = 1) => {
  let index = start;

  return {
    // for-of などによって反復可能なオブジェクトであることを示すフィールド
    [Symbol.iterator]() {
      return this;
    },
    // next メソッドを持つことで反復可能に
    next() {
      const value = index <= end ? index : undefined;
      const done = index > end;

      index += step;

      // next メソッドは必ず value と done を返す
      return {
        value,
        done,
      };
    },
  };
};

const it = makeRangeIterator(0, 20, 5);
let current = it.next();
while (current.done === false) {
  console.log(current.value); // 0, 5, 10, 15, 20
  current = it.next();
}
shingo.sasakishingo.sasaki

前述の makeRangeIterator みたいに、自前で next() 関数を持ったオブジェクトを生成するのは少々面倒であるため、ジェネレータ関数 (function*) でそれを代替することができる。

function* makeRangeIterator(start = 0, end = Infinity, step = 1) {
  let index = start;
  while (index <= end) {
    yield index;
    index += step;
  }
}

少々見慣れない構文ではあるが、上記関数は元の makeRangeIterator と同じ挙動となり、具体的には以下のように動作する。

  • 初回呼び出し時はコード実行自体はせずに、ジェネレーター(イテレーターのようなもの) を返す
  • ジェネレーターの next() メソッドが呼ばれるたび、次の yield が出現するまでコードを実行し、そのときの値を value として返す
  • yield が出現せずにコードが終了した場合、 done: true を返す
shingo.sasakishingo.sasaki

ジェネレーターは for-of などのイテラブルを受け取る構文とあわせて使用できる。

for (const iterator of makeRangeIterator(0, 20, 5)) {
  console.log(iterator); // 0, 5, 10, 15, 20
}
shingo.sasakishingo.sasaki

あるオブジェクトがイテラブルであることを示す手段として、 Symbol.iterator メソッドを定義する方法がある。

Synbol.iterator メソッドが、next() メソッドを持つオブジェクトを返す場合、それはイテラブルとして扱うことが出来る。

const makeMyIterable = (max = 100) => {
  let i = 0;
  return {
    [Symbol.iterator]: () => {
      return {
        next: () => {
          return {
            value: i++,
            done: i > max,
          };
        },
      };
    },
  };
};

for (const iterator of makeMyIterable(5)) {
  console.log(iterator); // 0, 1, 2, 3, 4
}

上記はジェネレータ関数で差し替えることも可能。

const makeMyIterable = (max = 100) => {
  let i = 0;
  return {
    *[Symbol.iterator]() {
      while (i < max) {
        yield i++;
      }
    },
  };
};
shingo.sasakishingo.sasaki

Array を例にしたが、他にも String Map Set なんかもイテラブルで、内部的には Symbol.iterator メソッドが実装されている。

for (const iterator of [1, 2, 3]) {
  console.log(iterator); // 1, 2, 3
}

for (const iterator of "abc") {
  console.log(iterator); // a, b, c
}

for (const iterator of new Set([1, 2, 3])) {
  console.log(iterator); // 1, 2, 3
}

for (const iterator of new Map([ [1, 2], [3, 4], [5, 6] ])) {
  console.log(iterator); // [1, 2], [3, 4], [5, 6]
}

試してみるとこんな感じ

const array = [1, 2, 3];
console.log(array[Symbol.iterator]()); // [object Array Iterator]
shingo.sasakishingo.sasaki

MDN には他にも細かいキーワードや挙動の違い、内部の仕組みなどの話もあるけど、基本的にはこれぐらい。

このスクラップは2022/10/10にクローズされました