ES2015から新たに導入された、イテレータ(iterator)とジェネレータ(generator)という概念についてまとめました。
イテレータ
イテレータ(反復子)は繰り返しのための機構で、以下の2つの種類のオブジェクトがあります。
この2つは相反するものでは無く、反復可能かつ反復子でもあるオブジェクトもあります。
反復可能なオブジェクト (iterable object)
Symbol.iterator
と呼ばれるプロパティを含んでいるオブジェクト、このプロパティの値はイテレータを返す関数for...of
を使って各要素について処理できる (配列、文字列、Map、Set)
反復子オブジェクト(iterator object)
配列についてみていく
配列は反復可能なオブジェクト (iterable object)です。
- 配列が反復可能なオブジェクトの例
const foodstuffs = [ 'egg', 'milk', 'sugar' ] for (let afood of foodstuffs) { console.log(afood) } /* 実行結果を表示 egg milk sugar */
しかし、配列はイテレータ(反復子オブジェクト)ではありません。
イテレータは上記の通りnext
というメソッドを呼び出す事で要素を取り出せるオブジェクトです。
先ほどの配列にnext
を使うと配列はイテレータ(反復子オブジェクト)では無いのでエラーになる
foodstuffs.next() /* 実行結果を表示 TypeError: foodstuffs.next is not a function */
しかし、values
というメソッドを使う事で配列をイテレータ(反復子オブジェクト)に変換する事ができる
// itという変数にイテレータを代入している const it = foodstuffs.values()
これによってイテレータを作成することができたのでnext
メソッドを使って要素を取り出すことができる
next
を呼び出すとvalue
とdone
の2つのプロパティをもつオブジェクトが返される
value
: 処理を行うために使う値done
: 全ての要素を返したかどうかを示す真偽値
作成したit
にnext
を使ってみる
const foodstuffs = [ 'egg', 'milk', 'sugar' ] const it = foodstuffs.values() console.log(it.next()) console.log(it.next()) console.log(it.next()) console.log(it.next()) /* 実行結果を表示 { value: 'egg', done: false } { value: 'milk', done: false } { value: 'sugar', done: false } // 供給するデータがなくなるとvalueはundefinedに、doneはtrueになる { value: undefined, done: true } */
イテレータは前の状態を記憶して、next
メソッドが呼ばれるたびに次のデータを供給している
イテレータのプロトコル
イテレータ(反復子オブジェクト)であるためには、イテレータプロトコルを実装する必要があり、そのために、Symbol.iterator
を実装します。
これは、イテレータの振る舞いをするオブジェクトを戻すものです。つまり、next
メソッドを呼ぶとvalue
とdone
をもつオブジェクトを返すようなメソッドを実装するということです。
詳しくはこちらを参考にしてください
class Food { constructor() { this.foodstuffs = [] } add(foodstuffs) { const now = Date.new() console.log(`食材追加: ${foodstuffs}(${now})`) this.foodstuffs.push({ foodstuffs, timestamp: now}) } [Symbol.iterator]() { return this.foodstuffs.values() } }
next
が呼ばれた時にvalue
とdone
をプロパティにもつオブジェクトを返すオブジェクト実装する方法
[Symbol.iterator]() { let i = 0; const foodstuffs = this.foodstuffs return ( next: () => i => foodstuffs.length ? {value: undefined, done: true} : {value: foodstuffs[i++], done: false} ) }
つまり、最初の例のfoodstuffs
は配列で、配列は反復可能なオブジェクト (iterable object)なので、[Symbol.iterator]
と持っています。
メソッド[Symbol.iterator]
は実行するとイテレータを返すので、配列は反復可能なオブジェクトをもとにイテレータを作成するのは簡単です。
配列のメソッド[Symbol.iterator]
を実行して返ってくる値を使えば良いです。
- 上の例をこのように書くこともできる
[Symbol.iterator]() { return this.foodstuffs[Symbol.iterator]() }
ジェネレータ
ジェネレータは関数の一種と考えることができますが、ジェネレータを呼び出しても関数のようにすぐには実行されず、まずはイテレータが返されます。その後、イテレータのメソッドnext
を呼び出すたびに実行が進んでいきます。
ジェネレータを定義する場合function
の後に*
をつけます。また、呼び出し元に値を提供するためにはyield
を使います(return
も使われますが通常は値を返すためには使われません)。それ以外は普通の関数と同じ構文です。
ちなみにジェネレータはアロー関数のように記述はできないそうです。
- ジェネレータの例
function* weeks() { yield '日' yield '月' yield '火' yield '水' yield '木' yield '金' yield '土' }
上で作成したジェネレータは以下のように呼び出すことができます。
ジェネレータを呼び出すとイテレータが返ってくるので、そのイテレータをnext
メソッドを使って順に処理していきます。
const it = weeks() console.log(it.next()) console.log(it.next()) console.log(it.next()) console.log(it.next()) console.log(it.next()) console.log(it.next()) console.log(it.next()) console.log(it.next()) /* 実行結果の表示 { value: '日', done: false } { value: '月', done: false } { value: '火', done: false } { value: '水', done: false } { value: '木', done: false } { value: '金', done: false } { value: '土', done: false } { value: undefined, done: true } */
return
ジェネレータのどこかでreturn
を呼び出すとdone
がtrue
になり、value
プロパティはreturn
で指定した値になります。
ジェネレータから有用な値を提供する場合yield
を使うべきです。
yield式と双方向コミュニケーション
ジェネレータを使うと呼び出し側との間で双方向の会話が可能になります。そのためにyield
を使います。
yield
は式なので、評価の結果何かしらの値を持ちます。どんな値かというとnext
呼び出し時の引数の値です。
ジェネレータは遅延評価を可能にしています。
- 双方向の会話ができるジェネレータの例
function* conversation() { const name = yield '名前は?' const food = yield '好きな食べ物は?' return `${name}さんの好きな食べ物は${food}だそうです!` } const it = conversation() console.log(it.next()) console.log(it.next('たろう')) console.log(it.next('オムライス')) console.log(it.next()) /* 実行結果の表示 { value: '名前は?', done: false } { value: '好きな食べ物は?', done: false } { value: 'たろうさんの好きな食べ物はオムライスだそうです!', done: true } { value: undefined, done: true } */
参考資料
O'Reilly Japan - 初めてのJavaScript 第3版