現代的JavaScriptおれおれアドベントカレンダー2017 – 23日目

概要

function* で、イテレータを返すジェネレータ関数を作成できます。中で yield が使えます。

function* createIdGenerator() {
  let currentId = 100;
  while (true) {
    yield currentId++;
  }
}

const iterator = createIdGenerator();
console.log(iterator.next().value);
console.log(iterator.next().value);
console.log(iterator.next().value);

これはオブジェクトを反復可能(イテラブル)にするのに便利です。

const obj = {
  *[Symbol.iterator]() {
    const max = 3;
    for (let i = 0; i < max; i++) {
      yield i;
    }
  }
};

for (let item of obj) {
  console.log(item);
}

イテレータ

著名なデザインパターンのひとつです。配列でないオブジェクトでも、共通のインターフェイスで反復処理を行えるようにします。

JavaScript (ES2015+) では(たぶん一般的な定義とは異なり)イテレータは next() メソッドを持つオブジェクトです。具体的なインターフェイスは後述します。

JavaScriptでのイテレータ

イテラブルなオブジェクトで [Symbol.iterator]() というすごい名前のメソッドを実行すると、イテレータを得られます。

const iterable = new Set([100, 200, 300]);

// イテレータ生成
const it = iterable[Symbol.iterator]();

while (true) {
  // 反復処理を進め、現段階の状態を得る
  const result = it.next();

  // 繰り返し条件を確認
  if (result.done) {
    break;
  }
  
  // 項目取得
  const item = result.value;

  console.log(item);
}

Set オブジェクトはイテラブルなオブジェクトなので、 [Symbol.iterator]() を実行するとイテレータ it を生成して返してくれます。

そのイテレータ it は next() メソッドを持ちます。このメソッド呼び出しを繰り返して反復 (iterate) していきます。

next() メソッドは実行するたびに、なんだろ、結果オブジェクト? result を生成して返します。結果オブジェクトって呼び方で良いかな。 IteratorResult という名前のインターフェイス(後述)を満たすオブジェクトです。

この結果オブジェクトはまた、二つのプロパティを持ちます。 value が反復途中の現段階の値で、 done がもう反復し終えたかが格納される真偽値です。

これらを使って配列でも何でもぐるぐるできます。

for-of で使う

オブジェクトがイテラブルである、つまり前述の各要素を満たす場合、 for-of を使って簡単に反復処理を実現できます。

const iterable = new Set([100, 200, 300]);

for (let item of iterable) {
  console.log(item);
}

わあ短い。

ジェネレータで生成する

[Symbol.iterator]() をちゃんと自作すればどんなオブジェクトでもイテラブルにできます。

next() メソッドを自前で実装しても良いんだけど、面倒なので、ジェネレータ関数というのを使って作ると簡単です。

ジェネレータ

function* を使ってジェネレータ関数を宣言できます。ジェネレータ関数を実行するとジェネレータオブジェクトを得られます。

function* createGenerator() {
  const values = [100, 200, 300];

  for (let i = 0; i < values.length; i++) {
    const item = values[i];
    yield item;
  }
}

const generator = createGenerator();

ジェネレータオブジェクトは加えてイテラブルでもあるで for-of で使えます。わーい。

for (let item of generator) {
  console.log(item);
}

generator[Symbol.iterator]() の結果は元のオブジェクト generator になります。

また同時にイテレータでもあるので next() を持ちます。コードは書かなくても良いかね。

yield

さらっと使ったけど、ジェネレータ関数の中では yield という式を使えます。この式は右辺を value 、また done: false という設定で、 next() で返す IteratorResult オブジェクトを生成します。

必ずしも for 文で使う必要はないです。

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

const it = oneTwoThree();

let result;
result = it.next();
console.log(result.value);  // => 1
result = it.next();
console.log(result.value);  // => 2
result = it.next();
console.log(result.value);  // => 3

[Symbol.iterator]() を実装する

いよいよ [Symbol.iterator]() です。 * を付けて、ジェネレータ関数というかジェネレータメソッドになります。

const obj = {
  *[Symbol.iterator]() {
    const max = 3;
    for (let i = 0; i < max; i++) {
      yield i;
    }
  }
};

for (let item of obj) {
  console.log(item);
}

他のイテレータ生成メソッドを作る

obj じゃなくて obj.iterator() のようにメソッドを呼んでやる必要があるけれど、他の名前でも大丈夫。

目的別に何通りか用意しても良いかもね。

const obj = {
  *iterator() {
    const max = 3;
    for (let i = 0; i < max; i++) {
      yield i;
    }
  }
};

for (let item of obj.iterator()) {
  console.log(item);
}

イテレータのインターフェイス

いっぱい出てきたのでまとめ。

  • 反復可能(イテラブル)なオブジェクト … [Symbol.iterator]() をもつオブジェクト( Iterable )
  • [Symbol.iterator]() … イテレータを返すメソッド
  • イテレータ … next() をもつオブジェクト( Iterator )
  • next()value 、 done を持つオブジェクト( IteratorResult )を返し、内部状態を進めるメソッド
  • value … 反復処理各段階における値
  • done … 反復処理が終了しているかどうか

[Symbol.iterator]()

Symbol.iterator というシンボルが、名前というか何だろ、識別子?になってるメソッドです。 [] は動的にプロパティ名を決めるやつです。別稿参照。あとシンボル Symbol についても。

なんならどこかで Symbol.iterator = 'iterate' とか定義されてると考えてください。

Iterable インターフェイス

オブジェクトがこのインターフェイスを満たしていると、 for-of とか ... とかが使えます。

  • [Symbol.iterator]() … イテレータオブジェクトを返す

Iterator インターフェイス

このインターフェイスを満たすオブジェクトをイテレータオブジェクトと呼びます。

  • next() … イテレータオブジェクトを返す
  • return() (optional) … イテレータオブジェクトを返す
  • throw() (optional) … イテレータオブジェクトを返す

return() を呼び出すと「もう終了するぞ」と、 throw() を呼び出すと「なんかおかしいぞ」を、呼び出し先となるオブジェクトに伝えることが、できる、そう、です。単純に配列みたいな情報を扱う上ではいらなさそうだけど、何か外部から情報を引っ張ってくるようなやつで使うときに便利なんだろか。わかんない。

IteratorResult インターフェイス

next() 他のメソッドはこのインターフェイスを満たすオブジェクトを返す必要があります。

  • done
  • value

分割代入やスプレッド演算子 ...

これら実はイテラブルオブジェクトが対象です。配列じゃなくても自作のオブジェクトでも、 [Symbol.iterator]() を備えていれば使えます。

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

// 分割代入
const [one, two, three] = oneTwoThree();
console.log(one);
console.log(two);
console.log(three);

// スプレッド演算子
function say(one, two, three) {
  console.log(one);
  console.log(two);
  console.log(three);
}
say(...oneTwoThree());

ES2018では普通のオブジェクトも分割代入したりスプレッド演算子で展開したりできるようになるっぽいんだけど、じゃあ普通のオブジェクトも全部イテラブルになるのかな? 仕様策定の様子は追ってないので思っただけだけど。

分割代入と ... の使い方は別稿参照。

その他

[Symbol.iterator]() て何だよ

なんで普通の名前じゃないんだ Symbol なんだ、 toString() みたいにしなかったんだ。

だいたいイテレータはイテラブル

「イテレータ」と「イテラブル」は別インターフェイスなので同時に満たす必要はないんだけど、内部で %IteratorPrototype% というイテレータ共通のプロトタイプがありまして、こいつがイテラブルだったりします。

配列とかのイテレータはこのプロトタイプを継承しているので、JavaScriptネイティブなイテレータはだいたいイテラブルになるっぽい。 TypedArray は専用のものを持たないが、普通の配列 Array の処理を利用している。

ちなみに %IteratorPrototype% の [Symbol.iterator]() は this を返します。

イテラブルではないイテレータ

の例。

class MyIterator {
  constructor(values) {
    this.values = values;
    this.index = 0;
  }

  // 自前で実装するぞ
  next() {
    const value = this.values[this.index];
    this.index += 1;
    return {
      value: value,
      done: this.index >= this.values.length,
    };
  }
};

const values = [100, 200, 300];

// イテレータとして利用
const it = new MyIterator(values);
let result;
result = it.next();
console.log(result.value);

// イテラブルとして利用(できない)
// Exception: TypeError: (new MyIterator(...)) is not iterable
for (let item of new MyIterator(values)) {
  console.log(item);
}

普通のイテレータ

Java方面の話で聞いたときは next() で次に移動しつつ値を戻り値でそのまま得て、続きがあるかどうかは hasNext() という別のメソッドを使うみたいな流れだったと思うんだけど、なんで違うのかしらん。

時代が変わってイテレータパターン自体が変わった?

インターフェイス vs プロトコル

ちなみにMDNだとイテレータとかで「プロトコル (protocol)」という表現が用いられているけれど、仕様書だと「インターフェイス (interface)」です。まあ同じものでしょ。

たしかSwiftはprotocolという表現してたよね。

function と * の間で改行できる

適当に空白文字を置ける様子。

function
* goo() {
}

async はだめだったのに~。

参考

更新履歴

  • 2017/12/23 TypedArray がイテラブルじゃないみたいな勘違いしてたのを修正