イテレータについて
概要
V8でES2015のイテレーション周りの実装が進んできたので、解説してみようと思う。
イテレーションとは
ここではデータの要素を繰り返して取り出すこと。
例えば配列のforEachメソッドは、要素やインデックスをイテレートするイテレータである。
ただしこれらは(内部的に)繰り返しまで行うイテレータ(内部イテレータ)だが、ES2015では外部イテレーションのための仕組みが入った。
イテレータオブジェクト
イテレータオブジェクトは『次の要素』を返すメソッドを備えているオブジェクトである。
つまりイテレーションの『次の要素を取り出す』ことだけを担い、それを繰り返すことは外で行われる(外部イテレータ)。
例
Array.prototype.values()は配列の要素を順番に列挙するイテレータオブジェクトを返す。
var ary = [5, 4, 3] var iter = ary.values() iter.next() // {value: 5, done: false} iter.next() // {value: 4, done: false} iter.next() // {value: 3, done: false} iter.next() // {value: undefined, done: true} iter.next() // {value: undefined, done: true}
このイテレータは単に値を返すのではなく、値と終了状態を含むオブジェクトを返している。
これがES2015におけるイテレータのインターフェイスの決まりとなっている。
逆に言えばES2015におけるイテレータとは、『{value: <Any>, done: <Boolean>}』を返す"next"メソッドを持っているオブジェクトのことである。
(ただしundefinedの時のvalue、falseのときのdoneは実際のところ必須ではない)
for-of文
イテレータオブジェクトを使ったイテレーションでは、反復処理を自分で記述しないといけないのだったが、それを代わりにやってくれる便利な構文がfor-of文である。
for-of文ではdoneが偽の間イテレータの"next"メソッドを繰り返し呼び、valueを渡してくれる。
var ary = [5, 4, 3] var iter = ary.values() for (var v of iter) console.log(v) /*log* 5 4 3 */
自作のクラスでも、イテレータを返すメソッドを実装すれば、同じように使える。
var catsProto = { toIterator: function () { var names = Object.keys(this) return names.values() } } var cats = { __proto__: catsProto, 'ミケ' : {age: 5, from: '京都'}, 'マリン': {age: 2, from: '佐賀'}, 'タロウ': {age: 4, from: '秋田'}, } for (var name of cats.toIterator()) console.log(name) /*log* "ミケ" "マリン" "タロウ" */
ただ、一々.toIterator()としないといけないのはやや大変だ。
(name of cats)としたいものであるが、実はそうするための仕組みが用意されている。
そこで登場するのが@@iteratorである。
@@iterator
実はfor-of文など、イテレータが期待される場面では、まず対象の「ビルトイン"iterator"シンボル(=@@iterator)」メソッドが呼び出される。
そしてそのメソッドが返した値がイテレータとして実際に扱われることになっている。
つまり"toIterator"の代わりに@@iteratorを実装すれば、必要な場面で暗黙的に呼んでくれるというわけである。
"toString"のイテレータ版だと思えばいい。
上の例を@@iteratorを使って、Array.prototype.valuesに頼らず書き直すとこうなる。(一例)
var catsProto = {} catsProto[Symbol.iterator] = function () { var names = Object.keys(this), i = 0 return { next: function () { return i < names.length ? {value: names[i++]} : {done: true} } } } var cats = { __proto__: catsProto, 'ミケ' : {age: 5, from: '京都'}, 'マリン': {age: 2, from: '佐賀'}, 'タロウ': {age: 4, from: '秋田'}, } for (var name of cats) console.log(name) /*log* "ミケ" "マリン" "タロウ" */
ここで1つ疑問に思うかもしれない。
for-ofが「@@iteratorメソッドを持つ(=イテラブル)」オブジェクトを期待しているのならば、最初のfor-ofの例のように直接イテレータを渡すことは出来ないはずである。
なぜなら、「イテレータはイテラブルでない(="next"メソッドを備えているオブジェクトは、必ずしも@@iteratorメソッドも備えているわけではない)」からである。
実は、ネイティブのイテレータは、【自分自身を返す】@@iteratorメソッドも備えている。つまり、イテラブルなイテレータとして実装されているのである。
"next"メソッドさえ備えていればとりあえずイテレータだと言えるが、この問題を起こさないため、【自分自身を返す】@@iteratorメソッドも備えさせた方がいいのかもしれない。
実装されるバージョン
・@@iteratorをサポートしたfor-of文
V8 3.27.28 3.28.64(デフォルト有効)
Chrome 38M?