uhyohyo.net

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

七章第三回 条件を満たすノードを順番に処理する:TreeWalker

このページの最終更新日:

今回は、たまに役立つものを紹介します。ある条件を満たすノード全てに対して処理を実行する方法です。

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

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

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

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

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

TreeWalkerのオブジェクトを作る

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

これには引数が3つ必要で、1つめの引数は頂点ノード、2つめと3つめは条件です。

頂点ノードとは、そのノード以下(そのノードとその子孫)を探索するというものです。例えば、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
全種類。

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

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

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


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

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

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もちゃんと判定されて、もし条件を満たしていたら満たしているということになります。

さて、今までのを踏まえて、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;
      }
  });

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

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

です。

TreeWalkerを使う

さて、こうしてできた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となります。逆も同じです。直感的で分かりやすいですね。

TreeWalkerオブジェクトが持つpreviousNode,nextNodeメソッドは返り値があり、移動後の位置にあるノードを返します。例えば、Bの位置にTreeWalkerがあった場合、nextNodeを呼び出すとDに移動し、返り値としてDのノードが返されるというわけです。

また、移動先がない場合はこれらのメソッドはnullを返します。上の例では、Jの位置でnextNodeを使ったりBの位置でpreviousNodeを使ったりした場合が該当します。その場合、移動先がないから、TreeWalkerの位置は変わりません。

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

これらの性質を利用すれば、nextNodeだけで条件を満たすノードを全て処理することができます。そのためには次のようにします。


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

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

順を追ってみていきます。また上のサンプルを使うことにしましょう。

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

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

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

Jの処理が終わったとき、tw.nextNode()nullを返します。Jの次のノードは無いからですね。よって変数nodeにもnullが代入されます。そして、nullはとして扱われるので、while文は無事終了します。

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

firstChild,lastChild,parentNode

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

ただし、firstChild,lastChildは、正確には「条件を満たす子孫の中で最初/最後のもの」に移動します。そのようなものがなければnullが返ります。

例えば、上の例で、Aの位置でfirstChild()を実行するとBに移動します。また、Bの位置でfirstChild()を実行するとDに移動します。ただし、それはCの位置でフィルター関数がFILTER_SKIPを返した場合です。もしFILTER_REJECTを返した場合には、その子孫は無視されるので移動先はDではなくEとなります。

lastChild()も同様で、Bの位置からlastChild()とするとEに移動します。またGからlastChild()とするとJに移動します。

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

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

previousSibling,nextSibling

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

これらの5つのメソッドは木構造上のノードを処理する順番を細かく制御したい場合に有用です。なお、firstChild,lastChild,parentNode,previousSibling,nextSiblingはノードが持つプロパティと同じ名前ですが、こちらはあくまでTreeWalkerが持つメソッドなので、区別しましょう。

実際のサンプル

では、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>