DOM 要素の集合のイテレーション (OO 版)

id:reinyannyan:20060404:p2 からの発展です。

Update [20060412]:

少しコードの見直しを行い、select の連結による多重絞込みを可能にしました (古い each の上に新しい each を次々と被せていくことで可能になりました。クロージャ万歳!)。

応用イテレータの追加

Ajax 版『はてなフォトライフデスクトップ』を開発する中で、XML ドキュメントのコレクションに対して inject を用いたい場面がありましたので、以下のように追加してみました:

var Domi = function (collection) {
  return {
    each: function (yield) {
      var item, i = 0;
      // while にしてみました。for よりも効率的でしょうか?
      while (item = collection[i])
	yield(item, i++);
    },

    inject: function (memo, yield) {
      this.each(function (item, idx) {
	memo = yield(memo, item, idx);
      });
      return memo;
    }
  };
};

試みにインターフェースを変えたんですが、どうでしょう?

Domi.inject(collection ...)

ではなく

Domi(collection).inject(...)

と呼び出す形になります。prototype.js の $A のように、いかにも collection を配列化したように見えますよね?

まぁそういう OO 的な見た目の良さはあるんですが、前回例として出した class_each のような応用的なメソッドの扱いをどうするか、という問題が残ります。

特殊化されたコレクションの再利用

前回のバージョンにも言えることなんですが、そのような特殊なメソッドを実装してしまうと、拡張性の点で問題が生じるんですね。

つまり、上のコードからも明らかなように、応用的なイテレータが依拠するのはプレーンな each ですから、class_each のようなものは蚊帳の外になってしまうわけです。

これは残念な事態と言わなければなりません。

class_each に基づいた class_inject なりを (もし必要だとしてです) いちいち定義する、という方法は拡張性の観点から論外です。

このような、特殊化されたコレクションを再利用することが果たして可能でしょうか? 配列化というオプションを除外しなければいけないこともあり、なかなか難しい問題ではないかと思います。

考えた結果、「クラス化」する案というのを思いつきました*1

DOM Iterator のクラス化

Domi (仮称) をクラスにしてしまえば、インスタンス・メソッド each を動的に特殊バージョンに書き換えることができ、「蚊帳の外」問題が解決できるはずです*2

そこで、Ruby や Prototype の select メソッドをヒントとして、次のように実装してみました (コーディングは、prototype.js の拡張コードに基づいています):

var Domi = Class.create({
  initialize: function (collection) {
    this.col = collection;
    this.len = collection.length;
  },

  each: function (yield) {
    // for に戻しました
    for (var i = 0; i < this.len; i++)
      yield(this.col[i], i);
  },

  inject: function (memo, yield) {
    this.each(function (item, idx) {
      memo = yield(memo, item, idx);
    });
    return memo;
  },

  select: function (cond) {
    var self = this, each = self.each;
    // each を書き換え
    this.each = function (yield) {
      var i = 0;
      each.call(self, function (item, idx) {
	if (cond(item, idx))
	  yield(item, i++);
      });
    };
    return this;  // 自分を返す -> select(...).inject(...) と繋げて利用可能に
  }
});

ポイントはコメントで記してみました。select メソッドで、コレクションから条件に当てはまるものを抽出し、それに対してさらにイテレータを適用できるようにしています。

また、select の利用を支援するものとして、任意のクラス名を選択するメソッド:

  select_class: function (className) {
    var pattern = new RegExp("(^|\\s)" + className + "(\\s|$)");
    return this.select(function (node) { return node.className.match(pattern) });
  }

を定義することもできます。

Prototype の getElementsByClassName とは意味の違いもあり、比較するのは適当ではないでしょう (第一、コレクションをプログラマが用意しなければならない点で確実に不便になっています)。が、要素の抽出がプログラマによって完全に自由に制御できるという点で、応用性の高いアプローチになっているのではないかと思います。

最後に

var $D = function (collection) {
  return new Domi(collection);
};

*1:もちろん、クラスのインスタンス化は毎回メモリーの確保を伴うことですから、関数版よりもやや重くなってしまうでしょう。しかし、ここでは配列化によるオーバーヘッドの問題が相当大きいものと想定していますので、このことは問題視しません

*2:書き換えた後で元に戻せば良いわけですから、クラス化にこだわる必要は実はあまりありません。今回は Domi の「使い捨て」の性質に着目して、後始末を考えなくて良い「クラス化」の方法を選びました