JS.next

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

イテレータについて

概要

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