「Firefox3でネイティブ実装の document.getElementsByClassName() が良い!」と聞きつけ、クロスブラウザ化

こちらが最新版です ⇒ http://d.hatena.ne.jp/uupaa/20090608/1244449986

まず、getElementsByClassName()と同様に、ノードリストを返すメソッド document.getElementsByTagName() が、何を返しているのか, document.getElementsByClassName() は実装されているのか調べました。

Browser document.
getElementsByTagName()
result type
document.
getElementsByClassName()
implemented
Firefox2(2.0.0.13) HTMLCollection No
Firefox3(Beta5) HTMLCollection Yes
Opera9(9.25) NodeList No
Opera9(9.50Beta) NodeList Yes
Safari3(3.1) NodeList Yes
IE6 Object No
IE7 Object No
IE8(Beta1) Object No

次に HTMLCollection と NodeList の対応状況を調べました。

Browser HTMLCollection NodeList
Firefox2(2.0.0.13) Yes Yes
Firefox3(Beta5) Yes Yes
Opera9(9.25) Yes Yes
Opera9(9.50Beta) Yes Yes
Safari3(3.1) No Yes
IE6 No No
IE7 No No
IE8(Beta1) No No

いつも通りバラけてますね。
Web Applications 1.0 によると、document.getElementsByClassName() が返すのは、生きている(Liveな) NodeList ですので、Firefoxは仕様と異なる型が返ってきています。

さて、document.getElementsByClassName() の代替実装付きテスト用コードです。(xsnapはXPathのスナップショットを取る関数です)

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>JavaScript::getElementsByClassName test</title>
<!--[if IE]><script type="text/javascript" src="../../lib/xpath.js"></script><![endif]-->
<style type="text/css">
body { background-color: black; color: white; }
.false { color: gray }
</style>
</head>
<body>

<div id="example">
 <p id="p1" class="aaa bbb"/>
 <p id="p2" class="aaa ccc"/>
 <p id="p3" class="bbb ccc"/>
</div>

<a href="http://www.whatwg.org/specs/web-apps/current-work/#getelementsbyclassname">WHATWG Web Applications 1.0: getElementsByClassName</a>

<pre>
&lt;div id="example"&gt;
 &lt;p id="p1" class="aaa bbb"/&gt;
 &lt;p id="p2" class="aaa ccc"/&gt;
 &lt;p id="p3" class="bbb ccc"/&gt;
&lt;/div&gt;

A call to <input type="button" value="document.getElementById('example').getElementsByClassName('aaa')" onclick="test1()" />
  would return a NodeList with the two paragraphs p1 and p2 in it.

A call to <input type="button" value="getElementsByClassName('ccc bbb')" onclick="test2()" />
  would only return one node, however, namely p3.

A call to <input type="button" value="document.getElementById('example').getElementsByClassName('bbb  ccc ')" onclick="test3()" />
  would return the same thing.

A call to <input type="button" value="getElementsByClassName('aaa,bbb')" onclick="test4()" />
  would return no nodes; none of the elements above are in the "aaa,bbb" class.
</pre>

<script>
function test1() { // document.getElementById('example').getElementsByClassName('aaa')
  uu.forEach(uu.enumClass('aaa', document.getElementById('example')), function(v) {
    alert(v.id);
  });
}
function test2() { // getElementsByClassName('ccc bbb')
  uu.forEach(uu.enumClass('ccc bbb'), function(v) {
    alert(v.id);
  });
}
function test3() { // document.getElementById('example').getElementsByClassName('bbb  ccc ')
  uu.forEach(uu.enumClass('bbb  ccc ', document.getElementById('example')), function(v) {
    alert(v.id);
  });
}
function test4() { // getElementsByClassName('aaa,bbb')
  uu.forEach(uu.enumClass('aaa,bbb'), function(v) {
    alert(v.id);
  });
}
</script>

<script>
var uu = window.uu = {
  enumClass: function(className, context /* = document */, tagName /* = undefined */) {
    var d = document, c = className, ctx = context || d, tag = tagName || '*';
    if (d.getElementsByClassName) { return ctx.getElementsByClassName(c); }
    var x = function(c) { return 'contains(concat(" ", @class, " "), " ' + c + ' ")'; };
    var rule = (c.indexOf(' ') >= 0) ? c.match(/\w+/g).map(x).join(" and ") : x(c);
    return uu.xsnap('.//' + tag + '[' + rule + ']', "", ctx, false);
  },
  xsnap: function(xpath, attr, context, sort) {
    var n = document.evaluate(xpath, context || document, null, sort ? 7 : 6, null);
    var rv = [], i = 0;
    attr = attr || "";
    if (attr.length) {
      for (; i < n.snapshotLength; ++i) { rv.push(n.snapshotItem(i).getAttribute(attr)); }
    } else {
      for (; i < n.snapshotLength; ++i) { rv.push(n.snapshotItem(i)); }
    }
    return rv;
  },
  forEach: function(map, iter, bindThis /* = undefined */) {
    if (!map || typeof iter !== "function") { throw TypeError(); }
    if (typeof map.forEach === "function") { return map.forEach(iter, bindThis); }
    if (map.length) { // Array like collection
      for (var i = 0, sz = map.length; i < sz; ++i) {
        (i in map) && iter.call(bindThis, map[i], i, map);
      }
    } else {
      for (var p in map) {
        map.hasOwnProperty(p) && iter.call(bindThis, map[p], p, map);
      }
    }
    return this;
  }
};
if (!Array.prototype.forEach) {
  Array.prototype.forEach = function(iter, bindThis /* = undefined */) {
    if (typeof iter !== "function") { throw TypeError(); }
    var i = 0, sz = this.length;
    for (; i < sz; ++i) { (i in this) && iter.call(bindThis, this[i], i, this); }
    return this;
  };
}
if (!Array.prototype.map) {
  Array.prototype.map = function(iter, bindThis /* = undefined */) {
    if (typeof iter !== "function") { throw TypeError(); }
    var rv = new Array(this.length), i = 0, sz = this.length;
    for (; i < sz; ++i) {
      if (i in this) { rv[i] = iter.call(bindThis, this[i], i, this); }
    }
    return rv;
  };
}
</script>

</body>
</html>

今回は、document.prototype.getElementsByClassName() とはしませんでした。

document.evaluate()が使えない環境(IE6/7/8)では、JavaScript-XPath等を使用してください。

テストコードは、Web Applications 1.0 にあるコードそのままです。
'ccc bbb'でも <p id="p3" class="bbb ccc"/> が探せなきゃいけないし、'bbb ccc 'でも探せなきゃいけないので実装がほんのちょっとだけ面倒くさいですね。
多くの人が getElementsByClassName() の独自実装を試みているようですが、
'ccc bbb'や'bbb ccc ' に対応している実装(仕様を満たしている実装)はあんまりないみたいです。