JavaScript の Iterator、関数とコンストラクタ2009年08月09日 16時21分

オンライン勉強会の Jetpack 入門に参加して Jetpack のソースコードを読んでいたら、Iterator を関数として呼び出したときとコンストラクタとして呼び出したときとでは挙動が違うということを知りました。

Iterator の動作

オブジェクト o に対して for-in 文、for-each-in 文を実行したとき、及び Iterator 関数、Iterator コンストラクタを呼び出したときの (SpiderMonkey の) 動作は、それぞれ次のようになります。

コード o__iterator__ メソッドを持つとき o__iterator__ メソッドを持たないとき
for (... in o) o.__iterator__(true) の返り値がイテレータとして使われる o の列挙可能なプロパティの名前を列挙する Iterator オブジェクトが作成され、イテレータとして使われる
for each (... in o) o.__iterator__(false) の返り値がイテレータとして使われる o の列挙可能なプロパティの値を列挙する Iterator オブジェクトが作成され、イテレータとして使われる
Iterator(o, keysOnly) o.__iterator__(Boolean(keysOnly)) の返り値が返される o 自身の列挙可能なプロパティの名前 (keysOnly が真と評価されるとき)、または名前と値を収めた配列 (そうでないとき) を列挙する Iterator オブジェクトが作成され返される
new Iterator(o, keysOnly) o 自身の列挙可能なプロパティの名前 (keysOnly が真と評価されるとき)、または名前と値を収めた配列 (そうでないとき) を列挙する Iterator オブジェクトが作成され返される o 自身の列挙可能なプロパティの名前 (keysOnly が真と評価されるとき)、または名前と値を収めた配列 (そうでないとき) を列挙する Iterator オブジェクトが作成され返される

列挙可能なプロパティとは ECMAScript でいう DontEnum 属性を持たないプロパティ、o 自身のプロパティとは o のプロトタイプチェーンをたどらずに得られるプロパティのことです。また、Iterator 関数及びコンストラクタの keysOnly 引数は省略可能であり、省略した場合は false を指定したものとして扱われます。

なお、JavaScript 1.7 では for-in 文で分割代入を使ったときにこれと異なる動作をしますが、JavaScript 1.8 で修正されたのでここでは触れません。

事例

var iteratorLikeObject = {
  array: ['p', 'q', 'r'],
  index: 0,
  next: function () {
    if (this.index < this.array.length)
      return this.array[this.index++];
    this.index = 0;
    throw StopIteration;
  }
};

var o = {
  a: 10,
  b: 20,
  __iterator__: function (keysOnly) {
    print('keysOnly = ' + keysOnly);
    return iteratorLikeObject;
  }
};

for (let i in o) print(i);
// => keysOnly = true
// => p
// => q
// => r

上のコードでは、__iterator__ メソッドを持つオブジェクトに対して for-in 文を実行しているので、__iterator__ メソッドの返り値である iteratorLikeObject がイテレータとして使われます。よって、列挙される値はその next メソッドの返り値である文字列 "p""q""r" となります。

ここで、Iterator 関数を使ってみるとどうなるでしょうか。

for (let i in Iterator(o)) print(i);
// => keysOnly = false
// => array
// => index
// => next

Iterator 関数の返り値は iteratorLikeObject なので、このオブジェクトに対して for-in 文が実行されます。しかし、iteratorLikeObject__iterator__ メソッドを持っていないため、そのプロパティの名前が列挙されます。

では __iterator__ メソッドを追加してみましょう。

iteratorLikeObject.__iterator__ = function (keysOnly) {
  print('keysOnly in iteratorLikeObject = ' + keysOnly);
  return this;
};

for (let i in Iterator(o)) print(i);
// => keysOnly = false
// => keysOnly in iteratorLikeObject = true
// => p
// => q
// => r

再び iteratorLikeObject に対して for-in 文が実行されますが、今度は __iterator__ メソッドが存在します。しかも、__iterator__ メソッドでは自分自身を返しているため、iteratorLikeObject がイテレータとして使われ、その next メソッドの返り値が列挙されます。

ここで、Iterator を関数ではなくコンストラクタとして使うと、そもそも o__iterator__ メソッドが呼び出されなくなります。

for (let i in new Iterator(o)) print(i);
// => a,10
// => b,20
// => __iterator__,function (keysOnly) {
// =>     print("keysOnly = " + keysOnly);
// =>     return iteratorLikeObject;
// => }

Iterator コンストラクタは、オブジェクト自身が持つプロパティを調べるときに便利です。

var o = {
  public1: 10,
  public2: 20,
  _private: 30,
  __proto__: {
    inherited: 40
  }
};
var keys = [i for (i in new Iterator(o, true)) if (!/^_/.test(i))];
print(keys.toSource());
// => ["public1", "public2"]

Iterator オブジェクト

Iterator 関数またはコンストラクタにより作られる Iterator オブジェクトは、next メソッドと __iterator__ メソッドを持ちます。他の組み込みオブジェクトのメソッドと異なり、これら 2 つのメソッドは上書きも削除もできません。

Iterator オブジェクトの __iterator__ メソッドは、引数によらず常に自分自身を返します。これにより、for (... in new Iterator(...))for each (... in new Iterator(...)) は同じ動作をすることになります。

var anyObject = {};
print(Iterator.prototype.__iterator__.call(anyObject) === anyObject);
// => true

ある Iterator オブジェクトが、どのオブジェクトのプロパティを列挙するものなのかは、__parent__ プロパティからわかります。

var anyObject = {};
var iterator = new Iterator(anyObject);
print(iterator.__parent__ === anyObject);
// => true