🐏

JavaScript のイテレーターを理解する

2024/11/18に公開

for...of などを使って反復処理可能なオブジェクトはどういう条件で成立するのか、また、そのような反復処理可能なオブジェクトはどのように実装できるのかを理解するためにイテレーターについて学んだ内容をまとめた記事です。

イテレーター

イテレーターとは

以下の2つのプロパティを返す next() メソッドを持っているオブジェクトのことです。

  • value
    • オブジェクトの次の値
  • done
    • オブジェクトの要素がまだ残っていれば false、全ての要素を取得し終えると true

一般的なイテレーターである配列で実際に next() メソッドを呼び出してみます。

// 配列を用意
const array = [1, 2, 3];

// 配列からイテレーターを取得
const iterator = array[Symbol.iterator]();

// イテレーターの next() メソッドを呼び出して結果を確認
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() の呼び出し3回目までは次の値があるので value に値が返ってきており、 done は false が返っています。
4回目の呼び出しでは、次の値がないため value は undefined になり、 done は true が返ってきています。

カスタムイテレーターの作成

以下のように、オリジナルのイテレーターを作成することもできます。

// 数値の範囲と増幅幅を指定してイテレーターを作成する関数
const makeRangeIterator = (start = 0, end = 100, step = 1) => {
  let nextIndex = start;
  let iterationCount = 0;

  const rangeIterator = {
    // next メソッドを実装
    next() {
      let result;
      if (nextIndex < end) {
        result = { value: nextIndex, done: false };
        nextIndex += step;
        iterationCount++;
        return result;
      }
      return { value: iterationCount, done: true };
    },
  };
  return rangeIterator;
};

// イテレーターを作成(1から10未満まで、2ずつ進む範囲)
const iterator = makeRangeIterator(1, 10, 2);

let result = iterator.next();
while (!result.done) {
  console.log(result.value); // 1, 3, 5, 7, 9
  result = iterator.next();
}

console.log(result.value); // 総イテレーション回数を出力(5)

実はこのカスタムイテレーターは、 for...of などで反復処理を実行するとエラーになってしまいます。

for (const value of iterator) {
  console.log(value) // TypeError: iterator is not iterable
}

これは、オブジェクトがイテレーターであることを認識するための実装が不足した状態であるためです。
これを解消するためには、最初のサンプルコードで登場した @@iterator メソッドの実装を追加する必要があります。

@@iterator メソッド

@@iteratorSymbol.iterator ) はオブジェクトが反復処理が可能であることを示す特別なメソッドで、反復処理可能なオブジェクトはこのメソッドがイテレーターオブジェクト自身を返すよう実装されています。
例えば、配列、文字列、Map 、Set などの組み込みオブジェクトは全て@@iterator メソッド がそのルールに則っているためfor...of などで反復処理を実行することができます。

先ほどのイテレーターの実装に @@iterator メソッドを追加してみます。

const makeRangeIterator = (start = 0, end = 100, step = 1) => {
  let nextIndex = start;
  let iterationCount = 0;

  const rangeIterator = {
    next() {
      let result;
      if (nextIndex < end) {
        result = { value: nextIndex, done: false };
        nextIndex += step;
        iterationCount++;
        return result;
      }
      return { value: iterationCount, done: true };
    },
    // @@iterator メソッドを実装
    [Symbol.iterator]() {
      // rangeIterator オブジェクトを返す
      return this;
    },
  };
  return rangeIterator;
};

const iterator = makeRangeIterator(1, 10, 2);

これでオブジェクトがイテレーターであることが認識できるようになり、反復処理が実行可能になります。

for (const value of iterator) {
  console.log(value); // 1, 3, 5, 7, 9
}

このイテレーターの実装ですが、ジェネレーターというもの使うことで簡潔で可読性の高いコードで実装可能です。

ジェネレーター

ジェネレーターは、function* 構文で定義する関数で、yield 文を使って値を1つずつ返すことができます。
ジェネレーター関数を呼び出すと、イテレーターが生成されます。これにより、イテレーターの手動実装よりも簡潔で可読性の高いコードを書くことが可能です。

以下はジェネレーターの基本的な実装例です。

function* simpleGenerator() {
  yield 1;
  yield 2;
  yield 3;
}

const generator = simpleGenerator();

console.log(generator.next()); // { value: 1, done: false }
console.log(generator.next()); // { value: 2, done: false }
console.log(generator.next()); // { value: 3, done: false }
console.log(generator.next()); // { value: undefined, done: true }

先ほどのイテレーターもジェネレーターを使うことで、以下のように簡潔に書くことが可能です。

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

const iterator = makeRangeIterator(1, 10, 2);

for (const value of iterator) {
  console.log(value); // 1, 3, 5, 7, 9
}

このコードでは、ジェネレーター関数が自動的に @@iterator メソッドを提供するため、for...of ループで直接使用できます。

まとめ

イテレーター

  • イテレーターは next() メソッドを持つオブジェクト
  • next() メソッドは value(次の値)と done(完了状態)を返す
  • @@iterator メソッドが実装されていることで、for...of などの反復処理で使用可能になる
  • 配列や文字列などは標準でイテレーターを持つ

ジェネレーター

  • function* 構文を使うことでイテレーターの実装は簡潔になる
  • yield 文を使い、状態を管理しながら値を返せる
  • ジェネレーター関数は自動で @@iterator を提供する

参考

Discussion