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