Ruby的イテレータ: each

JavaScriptで配列を扱う時に困った経験はないでしょうか? 例えば、オブジェクトのリストがあって、個々のオブジェクトに対して固有のクロージャ*1を割り当てたい場合、for ループで回すと、実際にクロージャが呼び出される頃にはカウンタは既に回り切っているので、どのクロージャを呼び出しても、一番最後のループの時の状態が再現されてしまう、といったことです。

分かりにくいと思うので簡単なコード例を:

function test()
{
    var objs = [obj1, obj2, obj3];
    for (var i = 0; i < objs.length; i++)
    {
	var msg = objs[i].id + ' was clicked';
	objs[i].onclick = function (){ alert(msg); };
    }
}

window.onload = test;

(obj1 等は適当なドキュメント・オブジェクト(の id)と考えてください)
この例において、どのオブジェクトをクリックしても "obj3 was clicked" と表示されてしまう、ということです。

こんな時、イテレータ*2が威力を発揮します。まず、イテレータを定義します:

Array.prototype.each = function (funcp, context)
{
    for (var i = 0; i < this.length; i++)
	funcp.call(context, this[i]);
};

これは、Ruby

array.each { |value| do_something_with value }

と似たメソッドを組み込みの配列オブジェクトに追加しています*3
funcp として関数ポインタを、context で funcp が実行される文脈*4を受け取ります。

で、さっきの関数を書き換えると:

function test()
{
    var objs = [obj1, obj2, obj3];
    objs.each(function (obj)
    {
	var msg = obj.id + ' was clicked';
	obj.onclick = function (){ alert(msg); };
    });
}

これで、期待通りの動作をしてくれます。each の中のブロックは実際には関数ですから、for ループとは違って毎回のループ(イテレーション)がフレッシュな、プライベートな空間になるためです。

こういったイテレータが使えるということを覚えると、JavaScript でのプログラミングが飛躍的に面白くなるんじゃないでしょうか。ということで、このテーマでもう幾つか書いてみたいと思います。

追記として:
FireFox の新しいバージョンで同様のメソッド群が追加されるようですが、私は根っからの IE ユーザーなので(信者ではありません、念のため)当面は気にしないことにします。

さらに追記:
さっき開発版の prototype.js のソースを見て気付きました。たっくさん配列メソッドがサポートされてますね(ハッシュまである)。ちょっと勉強してきまーす。

*1:http://en.wikipedia.org/wiki/Closure_%28computer_science%29

*2:http://en.wikipedia.org/wiki/Iterator

*3:prototype というプロパティに追加することで、全ての配列オブジェクトで利用できるようになります

*4:funcp が定義された場所、要はオブジェクトのこと。クロージャ内で this 指定子を使っている場合に必要