Smart Communication Design Company
ホーム > ナレッジ > Blog > Web標準Blog > 2010年10月 > DOMTokenListとspace-separated tokens

DOMTokenListとspace-separated tokens

2010年10月22日
フロントエンド・エンジニア 渡邉

先日のclassList (DOMTokenList) に関するエントリでは、DOMTokenListも用途によっては取り回しにくい面があると述べました。

結論からいえば、DOMTokenList各関数は空白文字区切りトークン (space-separated tokens) をとれないため、同一属性の複数属性値を操作しにくい場合があります。

classListgetElementsByClassName()の比較

ここからはDOMTokenListの利用例としてclassListに焦点を絞り、同じくclass属性に関係するgetElementsByClassName()と比較することで、とりうる引数の違いを明らかにします。

次の(X)HTML断片を例に説明します:

<p class="foo bar">first</p>
<p class="bar baz">second</p>
<p class="foo baz">third</p>

まずはgetElementsByClassName()から見ていきます。getElementsByClassName()classListと同様にHTML5で定義されたインタフェイスです(こちらは実装が先行したパターン)。getElementsByClassName()は引数にclass属性値をとり、それらを持つ要素ノードを収集します。

getElementsByClassName()の引数は、空白文字区切りトークン (space-separated tokens) です。つまり、複数のclass属性値を空白文字 (space characters) で区切り、AND条件として与えることができます。

次のコード断片は、空白文字で区切られたclass属性値foobarを両方持つ要素ノードのみを収集するgetElementsByClassName()の実行例です(CSSセレクタで例えれば.foo.barと同様であると考えることもできます):

var elems  = document.getElementsByClassName("foo bar"),
    nelems = elems.length,
    ret    = [],
    e;
for (e = 0; e < nelems; e++) {
    ret[e] = elems[e].className;
}
// "foo bar" のみアラート
alert(ret.join(', '));

一方、classListの各関数(add()remove()contains()toggle())は、それぞれclass属性値 (DOMString) のトークンを引数とします。これは仕様にも明記されているように、空白文字区切りトークン (space separated tokens) ではないので、空白文字を含めることができません(引数に空白文字が含まれている場合、例外INVALID_CHARACTER_ERRが投げられる)。

次のコード断片は、containes()により対象要素ノードが特定のclass属性値を含んでいるか否かをテストしようとしている例です。しかし、最終行のcontains()引数が空白文字を含んでいるため、結果的に例外が投げられてしまいます:

var elem = document.querySelector(".foo.bar");
elem.classList.contains("baz");     // 問題なし、false を返す
elem.classList.contains("foo bar"); // 例外が投げられる

文字列リテラルの分割と「空白文字」の違い

前述のように、DOMTokenList各関数は複数の属性値を同時に扱うことができません。よって、一度に複数の属性値を操作したい場合は、対象とする属性値を(ループを回すなどして)1つずつ与える必要があります。

例えば、文字列リテラル"alpha beta gamma"を空白文字区切りトークンとみなして、それらすべてが特定要素ノードのclass属性値として含まれるかどうかをclassListcontains()でチェックしたいとしましょう。このとき、文字列リテラル"alpha beta gamma"split()を用いて分割、Arrayオブジェクト化して配列の各アイテムごとに処理を行うようにする――といった実装を考えることができます。次のコード断片は、前述の実装を試してみた例です:

var target   = document.getElementById("dummy"),
    classes  = "alpha \t \n beta \f \r gamma".split(/[ \t\n\f\r]+/),
    nclasses = classes.length,
    cl       = target.classList,
    result   = true,
    c;
// 線形探索
for (c = 0; c < nclasses; c++) {
    if (!cl.contains(classes[c])) {
        result = false;
        break;
    }
}
// 1つでも含まれていなければ false をアラート
alert(result);

こうした実装は、単純に考えればネイティブコードに引数を与えるだけの場合よりオーバーヘッドが増えやすくなります(もちろん、時と場合によって影響の度合いは様々です)。また、文字列リテラルをsplit()を用いて分割する際にはHTML5における空白文字 (space characters) の仕様を知っておく必要があります。HTML5における空白文字とJavaScript(ECMAScript 5 ないし ECMAScript 3)の正規表現における\sの範囲が異なっているからです。

DOMTokenListは便利ですが、特に複数の属性値を操作する場合には注意が必要です。しかし、前述したような仕様の詳細を念頭において使いさえすれば問題はないでしょう。