Jintrick.netagenda2008年02月アーカイブ → 2008年02月01日

Javascriptのイテレータ備忘録

イテレータについてはHawk's Laboratory » JavaScript 1.7の新機能が詳しいけれども、一応自分用のメモとして残しておく。

Javascriptにおけるイテレータは抽象クラス様のオブジェクトであり、nextメソッドを持つことだけが要求される。ちなみにJavascriptといったらここではJavascript1.7(以下略)。

次のようなコードを考える:

for each文、for in文におけるループでは、そのオブジェクト__iterator__プロパティが(あれば)callし、それをイテレータとして利用する。ループ毎にそのnextメソッドがcallされその戻り値が変数に格納される。nextメソッドがStopiteration例外を投げると暗黙的にcatchしそして自動的にループが終了する。ちなみにvar演算子ではなくlet演算子で変数を宣言すると、for each, for inブロック中の局所変数とすることができる。

イテレータのエッセンスを紹介する例として、for each(let i in new Range(10)){}という文でブロック内の処理を10回繰り返す、という「Rangeオブジェクト」を定義してみよう。

Rangeオブジェクト
コンストラクタ
function Range(num){
	this.roopLimit = num;
}
__iterator__プロパティ
Range.prototype.__iterator__ = function(propOnly){
	if(propOnly) return undefined;
	var 	limit = this.roopLimit,
		it = new Object,
		i = 0;
	it.next = function(){
		if (i >= limit){
			i = 0;
			throw StopIteration;
		}
		return i++;
	};
	return it;
};
使用例
for each(let i in new Range(10))
	alert("hello") // "hello"と10回アラートされる。

Rangeオブジェクトの__iterator__()が返すイテレータは、1) nextメソッドを持っていて、2) 一定回数以上呼び出されるとStopIterationを投げて自身を初期化する。この2点を満たしていれば何でも良い。したがって上の例はジェネレータを使った方が明らかにベターなのだが、構造が隠蔽されてしまうので説明には不向きであった。他には自分自身をイテレータにしてしまう方法もあり、それぞれに利点と欠点がある。

ジェネレータを利用する場合
Range.prototype.__iterator__ = function(){
	for (let i=0; i<this.roopLimit; i++) yield i;
};

ジェネレータはnextメソッドを持ち、ループが終わると自動でStopIterationを投げてくれるので、ジェネレータを使えば最も簡潔に書けるが、ここではジェネレータの特性についてこれ以上の説明はしない。

自身をイテレータにする場合
定義
function Range(num){
	this.roopLimit = num;
	this.__index = 0;
}
Range.prototype.__iterator__ = function(){ return this};
Range.prototype.next = function(){
	if (this.__index >= this.roopLimit)
		throw StopIteration;
	return this.__index++;
};
使用例
var rng = new Range(10);
rng.next();
for each(let i in rng)
	alert("Hello"); // "hello"と「9回」アラートされる

自身をイテレータにする利点は、自分のメソッドとしてnextメソッドを明示的に使用できることである。

for in文でもcallされる__iterator__プロパティ

上のRangeオブジェクトを使ったループはfor in文でも全く同様に動作する。即ち上のような__iterator__プロパティはfor each文とfor in文のセマンティクスの違いを吸収するという副作用を持っている。この点には十分に注意して使う必要があるだろう。for each文とfor in文でセマンティクスを変えたいなら、__iterator__がcallされる際、for each文ではfalse、for in文ではtrueが引数として渡されるのを利用すれば良い。

さらに__iterator__は、そのオブジェクトのプロパティと値のセット(リスト)を列挙してくれるIterator関数さえ「上書き」してしまう。しかしIterator関数の第二引数にtrueを指定すればプロパティ名のみを列挙する組み込みのイテレータが返される。 繰り返すと、__iterator__プロパティを定義した先述のRangeオブジェクトに関して、次の3つは全て同じ挙動をする:

デフォルトのfor in文の効果を得たいなら、次のようにする:

演習:NamedNodeMapの各ノードの名前と値をfor each文で直接参照できるようにする

NamedNodeMapというのは属性コレクションなどのインターフェイスで、簡単な例ではdocument.body.attributesなどが挙げられる。例えばbody要素の全属性の名前と値を列挙したいとき、次のように書けるよう、イテレータを利用してみよう:

NamedNodeMapを便利に使う例
for each(let [name, value] in document.body.attributes)
	document.write(name + ": " + value);

Python風のlet [name, value]という書き方(Destructuring Assignment)はJavascript1.7の特権である。一見大したことはないのだが、何故かこれができるとできないとではコードの生産性が思った以上に違ってくる。

しなければならないのはたった一つ。NamedNodeMap.prototype__iterator__を追加してやればいい。

NamedNodeMapに追加する__iterator__プロパティ
NamedNodeMap.prototype.__iterator__ = function(nodeOnly){
	if (nodeOnly)
		for (let i=0, len=this.length; i<len; i++)
			yield this.item(i)
	else
		for (let n, i=0, len=this.length; i<len; i++) {
			n = this.item(i);
			yield [n.nodeName, n.nodeValue];
		}
};

for in文でこれがcallされたときのみnodeOnlytrueなので、次の例は属性オブジェクトが列挙される:

for in文では……
for (let attr in document.body.attributes)
	document.write(attr.nodeName, attr.value);

自分自身をイテレータにする例については、Javascript1.7のカスタムイテレータとXPath (agenda)の_NodeListオブジェクトが適当な例だろう。ノードリスト以外の別の列挙体に基づいた処理をした後(nextメソッドを使う)、残りのノードをfor in文で列挙する、といった柔軟な処理を簡潔に記述できる。

自身をカスタムイテレータとして有効に利用する例
var nl = document.selectNodes("/descendant::p");
for (let i in new Range(30)) {
	try {
		let e = nl.next();
	} catch (err if err instanceof StopIteration) {
		break;
	}
	e.id = "id" + i;
}
for (let e in nl)
	e.changeClass("over30");

まとめ

命題列挙。

Javascriptで「クロスブラウザ」なんてことができると思うのは誤り。こてこての「クロスブラウザ」なスクリプトも、このJavascript1.7なスクリプトも、実は互換性という意味では大差がない。互換性を高めたければ、Javascriptなしでも利用可能にしておくこと。これに尽きる。


webmaster@jintrick.net
公開: 2008年02月01日
カテゴリ: DOM ,Javascript