uhyohyo.net

JavaScript初級者から中級者になろう

七章第三回 条件を満たすノードをまとめて処理する: TreeWalker

今回も、役立つものを紹介します。ある条件を満たすノードを、まとめて処理する方法です。

同じ感じのことは、今までに多少やってきました。例えば、getElementsByTagName(二章第六回)は、あるタグ名を持つ要素の一覧を得るためのものです。そうして得たNodeListをfor文などで1つずつ処理すれば、「ある要素名を持つ」という条件を満たすノードをまとめて処理したということになります。

しかし、今回紹介する方法では、要素名だけでなく、もっと複雑な条件を指定できます。

今回紹介するのは、TreeWalkerです。

直訳すると「木を歩くもの」という意味です。木とは木構造のことで、分かりにくいと思いますが、木構造の上を移動しながら次々処理をしていくというイメージです。

とりあえずやってみましょう。

TreeWalkerのオブジェクトを作る

TreeWalkerを使うには、まずTreeWalkerのオブジェクトを作る必要があります。それには、documentのcreateTreeWalkerというメソッドを使います。

これには引数が4つ必要で、1つめの引数は頂点ノード、2つめと3つめは条件、4つめは実体参照を展開するかどうかです。

頂点ノードとは、そのノード以下(そのノードとその子孫)を探索するというものです。例えば、body要素を指定すると、そこが頂点ですから、その親のhtml要素や兄弟のhead要素は探索されません。文書全体を探索するには、ルート要素であるhtml要素か、あるいはその上であるdocument自体を指定するといいです。ちなみに、ルート要素は、document.documentElement(三章第五回)で取得できます。

2つめと3つめは条件です。TreeWalkerを作るときに、もう条件を決めてしまうわけですね。できたオブジェクトは、その条件専用のTreeWalkerになります。そして、なぜ引数を2つも条件に使うかですが、それには意味があります。

まず2つめの引数では、おおまかな条件を指定します。この大まかな条件は、ノードの種類による絞り込みを行います。ノードの種類は、たとえば「要素ノード(HTMLElement)」とか、「テキストノード」とかです。

これは数値で指定しますが、前回と同じように定数があります。使えそうなのをいくつか紹介します。

SHOW_ELEMENT
要素ノードのみ。
SHOW_TEXT
テキストノードのみ。
SHOW_CDATA_SECTION
CDATAセクション(六章第一回)のみ。
SHOW_COMMENT
コメントノードのみ。
SHOW_ALL
全種類。

これらの定数を引数として渡すわけですが、今回もJavaScriptでどうやって利用するのか解説します。実は、NodeFilterという名のついたオブジェクトがあり、これのプロパティとして参照できます。

さらに、これらの定数は、1つのビットのみが立った数値で、2つ以上のビットが立った値を渡した場合、その両方が対象となります。1つのビットのみが立った数2つからその両方のビットが立った値を得るには、ビットごとの論理和を使うのでした。だから、例えば「要素ノード」と「テキストノード」の二種類を対象にしたいとき、

NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT

のようにします。これは前回紹介したcompareDocumentPositionと似た感じですね。

さて、3つめの引数では、さらに詳しい条件を指定できます。3つめの引数に指定するのは関数です。この関数はフィルターと呼ばれ、引数としてノードが渡されます。そのノードが条件を満たすかどうかを判定し、返り値で判定結果をTreeWalkerに教えます。だから、例えばこんな感じです。

function (node){
    if(node.className == "aaa"){
        return NodeFilter.FILTER_ACCEPT;
    }else{
        return NodeFilter.FILTER_SKIP;
    }
}
          

返り値として、よくわからないものを返しています。実は、これも定数で、上と同じようにNodeFilterのプロパティです。

FILTER_ACCEPT
そのノードは条件を満たす。
FILTER_REJECT
そのノードは条件を満たさず、その子孫も全て条件を満たさない。
FILTER_SKIP
そのノードは条件を満たさない。

の3種類があります。今回の場合そのノードのクラス名が"aaa"であれば(ただしこの方法は"aaa bbb"のように複数のクラスに属する可能性があるから不完全です。十二章第一回で紹介するclassListを使いましょう)、FILTER_ACCEPTを返すから条件を満たしているということになります。

そうでない場合(else)、FILTER_SKIPを返していて、条件を満たさないということになります。

今回使われていないFILTER_REJECTですが、これも条件を満たさないことを意味します。FILTER_SKIPとの違いは、FILTER_REJECTの場合、その子孫も一緒に問答無用で条件を満たさないことになります。例えば、

 A
|
├―― B
|  |
|  ├―― C
|  |
|  └―― D
|
└―― E
          

という木構造の場合、BでFILTER_REJECTが返されたら、C,Dが条件を満たしていたとしても、B以下は全て条件を満たさないということにされます。EはBの子孫でないから、影響は受けません。

FILTER_SKIPの場合は、子孫に影響を与えません。上のサンプルの場合、BがFILTER_SKIPだとしても、C,Dもちゃんと判定されて、もし条件を満たしていたら満たしているということになります。

最後に、4番目の引数ですが、基本的に深く考えずにfalseでいいです。これは実体参照(EntityReference)ノードが文書中に存在する場合にそれをそのまま残すか展開するかのオプションなのですが、たいていの処理系は実体参照を読み込み時に展開してしまうので、基本的にはfalseでもtrueでも違いがありません。

さて、今までのを踏まえて、createTreeWalkerを使ってみましょう。条件は、「クラス名が"abc"である要素」ということにしてみます。

var tw = document.createTreeWalker( document,
NodeFilter.SHOW_ELEMENT,
function(node){
    if(node.className == "abc"){
        return NodeFilter.FILTER_ACCEPT;
    }else{
        return NodeFilter.FILTER_SKIP;
    }
},
false);
          

何行もありますが、これはただcreateTreeWalkerを呼んでいるだけです。見やすいように、引数1つごとに改行をしています。1つめの引数がdocument、2つめがNodeFilter.SHOW_ELEMENTで、3つめが

function(node){
    if(node.className == "abc"){
        return NodeFilter.FILTER_ACCEPT;
    }else{
        return NodeFilter.FILTER_SKIP;
    }
}

で、4つめがfalseです。

TreeWalkerを使う

さて、こうしてできたTreeWalkerオブジェクトを使ってみましょう。

TreeWalkerの「位置」

TreeWalkerには、位置というものがあります。TreeWalkerはどこかのノードのところに居ます。そして、ノードからノードへと動き回るのです。

TreeWalkerが動くノードは、それに設定された条件を満たすノードのみです。木構造の上を順番に動いていってひとつずつ処理することで、条件を満たすノードをまとめて処理するという目的を果たすのです。

そして、TreeWalkerを動かすのには、TreeWalkerのメソッドを使います。

previousNode,nextNode

previousNodenextNodeは、TreeWalkerを動かす基本的なメソッドのひとつです。それぞれ、「前のノード」「次のノード」という意味です。

これは、親とか子とかは関係なく、ただ単に「前」「次」のノードです。例えば、

 A
|
├―― B
|  |
|  ├―― C
|  |  |
|  |  └―― D
|  |
|  └―― E
|     |
|     └―― F
|
└―― G
   |
   ├―― H
   |
   └―― I
      |
      └―― J
          

という木構造があり、B,D,E,G,H,Jが条件を満たしている場合を考えます。

このとき、Bの次はD、その次がE,G,H,Jとなります。逆も同じです。直感的で分かりやすいと思います。

JavaScriptでは、previousNode,nextNodeを使って次のようにします。

//(twはTreeWalkerオブジェクト)
var node = tw.nextNode();
var node2= tw.previousNode();
          

これらのメソッドには返り値があり、移動後の位置にあるノードのオブジェクトを返します。つまり、例えば最初にBの位置のTreeWalkerがあった場合、nextNodeでDに移動し、変数nodeにはDのノードが入るというわけです。

その後previousNodeで前に移動するとBの位置に移動するから、previousNodeはBを返します。

また、Jの位置でnextNodeを使ったり、Bの位置でpreviousNodeをつかった場合、nullを返します。その場合、移動先がないから、TreeWalkerの位置は変わりません。

そして、TreeWalkerが作られた直後は、TreeWalkerの位置は特に定まっていません。まだどこにもないという状態ですね。この最初の状態でnextNodeを使うと、最初のノード、つまり上の例の場合Bに移動するということになっています。

これらの性質を利用して、nextNodeだけで条件を満たすノードを全て処理することができます。この方法が、TreeWalkerでもっともよく使われると思います。

//(twは作ったばかりのTreeWalkerオブジェクト)
var node;
while( node = tw.nextNode() ){
    node;	//そのノードに対する処理
}
          

という形です。

whileの条件が代入文になっていますね。代入演算子は代入されたその値を返すということは基礎編第二回で解説しました。つまり、whileで何回も処理される処理の開始時に変数nodeにnextNodeで移動先のノードを代入し、同時にその値をwhileの条件にしているということです。

順を追ってみていきます。また上のサンプルを使ってみます。

まず、while文に入ると条件式でtw.nextNodeが実行されます。上で解説したとおり、最初のノードであるBが返されます。オブジェクトなら真なので、whileの条件は真となり実行されます。その処理では、変数nodeにBが入っているから、まずBに対して処理ができました。

次、nextNodeはDを返し、TreeWalkerもDの位置に移動します。同様にnodeにはDが入り、Dが処理されます。

以下同様に、E,G,H,Jと処理していきます。

Jの処理が終わったとき、次にnextNodeはJの次がないからnullを返します。変数nodeにもnullが代入されます。そして、nullはだから、while文は無事終了します。

このようにして、nextNodeだけで条件を満たす全てのノードを処理することができました。正直、これさえわかれば十分という感じもしますが、一応他のも紹介しておきます。

firstChild,lastChild,parentNode

この3つはいずれもメソッドです。firstChildは「最初の子」、lastChildは「最後の子」、parentNodeは「親ノード」という意味です。それぞれ対応するノードの位置に移動してそのノードを返すというメソッドです。

ただ、firstChild,lastChildは、「条件を満たす子のなかで最初のノード」、「条件を満たす子のなかで最後のノード」です。条件を満たす子がひとつもなければ、nullとなり移動しません。また、対象となるのは直接の子だけということになっています(実際はブラウザによって動作が違ったりします)。

例えば、上の例で、Aから見たらfirstChildはBで、lastChildはGです。Bからみたら、firstChildもlastChildもEです。

また、parentNodeでは、条件を満たす親ノードに移動します。しかし、親ノードはもともと一つしかないですね。もしその親ノードが条件を満たしていない場合は、そのさらに親を探して移動するということになっています。親の親がだめなら親の親の親…のように探します。最終的に頂点ノードまで探しても無かったら、nullとなり移動しません。

例えばDの位置から見たparentNodeは、直接の親のCが条件を満たさないからその上のBが見られ、Bが条件を満たすからBに移動するということになります。

なかなか使いにくいのであまり使われません。

previousSibling,nextSibling

previousSiblingは「前の兄弟ノード」、nextSiblingは「次の兄弟ノード」にそれぞれ移動します。previousSiblingは直前の兄弟が条件を満たさなければもうひとつ前、それも満たさなければさらに前…のようにして探します。なければいつものようにnullです。nextSiblingも同様です。

firstChild,lastChild,parentNode,previousSibling,nextSiblingはノードが持つプロパティと同じ名前ですが、こちらはあくまでTreeWalkerが持つメソッドなので、区別しましょう。

実際のサンプル

実際に使ってみましょう。今回は、「p要素またはdiv要素の背景色を黄色にする」ということをやってみます。本当は第五章で解説した方法でやるのがいいですが、サンプルだし気にしません。

<!doctype html>
<html>
  <head>
    <title>test</title>
  </head>
  <body>
    <p>p1</p>
      <div>div1</p>
    <p>p2</p>

    <script type="text/javascript">
      var tw = document.createTreeWalker(document,
      NodeFilter.SHOW_ELEMENT,
      function(node){
          if(node.tagName=="P" || node.tagName=="DIV"){
              return NodeFilter.FILTER_ACCEPT;
          }else{
              return NodeFilter.FILTER_SKIP;
          }
      },
      false);

      var node;
      while(node = tw.nextNode()){
          node.style.backgroundColor="yellow";
      }
    </script>
  </body>
</html>