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
最近のコメント