JS.next

JavaScriptの最新実装情報を追うブログ

イテレータについて

概要

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?