30代からのプログラミング学び直し!

10年エンジニアやってるけどいまだになんもわからん

JavaScriptのイテレータとジェネレータをちょっと理解した

イテレータとジェネレータという言葉だけで、なんか難しそう…と思っていたのですが、ちゃんと勉強してみたらそんなに恐れることでもなかったです。

反復可能(イテラブル)とは

イテレータは、オブジェクトを反復可能にするよ。反復可能っていうのは、for/ofでループ(巡回)したりスプレッド演算子で展開できたりするってこと。 配列は反復可能。文字列やSetオブジェクトやMapオブジェクトも反復可能。それ以外のオブジェクトはそのままだと反復できない。

console.log(...[1, 2, 3]); // => 1 2 3
console.log(...{ a: "aaa", b: "bbb" }); // TypeError!
イテレータでオブジェクトリテラルを反復可能にしてみる

オブジェクトを反復可能にするには、最低限、以下の3つを設定すればok。これはお作法だと思っとく。

  • [Symbol.iterator]というSymbolの名前を持つメソッドを用意する。
  • その中で、 next() というメソッドを持つイテレータオブジェクトを必ず返す。
  • next()メソッドは、 value プロパティと done プロパティ(論理値型)を持つオブジェクトを返す。反復処理では、doneプロパティがtrueになるまで、next()オブジェクトが繰り返し呼び出される。

オブジェクトを反復した時に、プロパティのキー/値の配列を返却するようにしてみる。

let obj1 = {
  a: "aaa",
  b: "bbb",
  [Symbol.iterator]() {
    let e = Object.entries(this);
    let i = 0;  // next() が呼ばれる度にインクリメントする
    // イテレータオブジェクトを返却する
    return {
      next() {
        return i < e.length ? { value: e[i++] } : { done: true };
      },
      // これはなくても(このコードでは)動くけど、イテレータ自体も反復可能にしておくと便利
      [Symbol.iterator]() {
        return this;
      },
    };
  },
};
console.log(...obj1); // => [ 'a', 'aaa' ] [ 'b', 'bbb' ] : エラーにならなくなった!

本当はもっと複雑な設定とかあるけど、これが一番シンプルな実装。

ジェネレーターでもっと簡単に実装する

イテレータ便利だけど、ループが途中でbreakされた場合とか色々考えるとちょっと複雑になってしまう。ので、もっと簡単にイテレータを作成するためにジェネレータがある。 ジェネレータは、 function*(functionキーワードは省略できたりする) と yield(または yield*)というキーワードを使う。

さっきのコードをジェネレータで書き換えるとこうなる

let obj2 = {
  a: "aaa",
  b: "bbb",
  *[Symbol.iterator]() {
    for (let e of Object.entries(this)) {
      yield e;
    }
  },
};
console.log(...obj2); // => [ 'a', 'aaa' ] [ 'b', 'bbb' ]

next()とかvalue doneとかを自分で設定する必要がなくなって、yield文の値が、イテレータの戻り値になるので、簡潔に書ける!「ループされた時には、yieldの値を呼び出しますからね!」っていう設定だととりあえず思っとく。

ちなみに yield* は、全部の yieldをまとめて返してくれるので、再帰処理とかに便利。

ジェネレータを関数とかクラスとかで使う

以上の例はオブジェクトリテラルでやってみたけど、実際には関数とかクラスとかで使うことの方が多そう。なのでいくつか例を書いておく。

関数 (引数で与えた範囲の整数を列挙する)

function* range(from, to) {
  for (let i = Math.ceil(from); i <= to; i++) yield i;
}
console.log(...range(3, 5)); // => 3 4 5

クラス(コンストラクタで与えた範囲の整数を列挙する)

class Range {
  constructor(from, to) {
    this.from = from;
    this.to = to;
  }
  *[Symbol.iterator]() {
    for (let x = Math.ceil(this.from); x <= this.to; x++) yield x;
  }
}
console.log(...new Range(3, 5)); // => 3 4 5

オブジェクトのメソッド省略記法(プロパティのキー/値を列挙する)

let obj = {
  a: "aaa",
  b: "bbb",
  *keyvalues() {
    for (let e of Object.entries(this)) {
      yield e;
    }
  },
};
console.log(...obj.keyvalues()); // => [ 'a', 'aaa' ] [ 'b', 'bbb' ] [ 'keyvalues', [GeneratorFunction: keyvalues] ]

簡単に書くとこんな感じ。 ジェネレータを使い所で適切に使うことができるようになれば、脱初心者!って感じがします🤓