uhyohyo.net

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

七章第二回 ノードどうしの位置関係を知る

今回はあるひとつのメソッドを紹介します。それは、題にある通り、ノードどうしの位置関係を知るというものです。いきなりサンプルを見てみましょう。

<!doctype html>
<html>
  <head>
    <title>test</title>
  </head>
  <body>
    <p id="p1">p1</p>
    <p id="p2">p2</p>

    <script type="text/javascript">
      var p1 = document.getElementById('p1');
      var p2 = document.getElementById('p2');

      console.log(p1.compareDocumentPosition(p2) );
    </script>
  </body>
</html>

4が表示されます。

ここで登場したメソッドcompareDocumentPositionが、ノードどうしの位置関係を表します。やってみて分かるように、返り値は数値です。

引数は一つで、

node.compareDocumentPosition(other)

(nodeとotherはノードのオブジェクト)という書き方があったとき、「nodeから見たotherの位置」を結果として返します。この関係を覚えるのがなかなかややこしくて面倒だったりもします。それでは、この「4」はどんな位置であることを表しているのでしょうか。

ここで木構造を見てみます。

body
|
├―― p1  ←node
|
└―― p2  ←other
          

otherは、nodeより後ろにあります。実は、このことをこの数値「4」が表しています。

しかし、4といわれても何のことかすぐには把握できません。そこで、定数があります。定数とは何かというと、五章第二回で解説したもので、ある意味をもつ数値に名前をつけて、意味が分かりやすいようにしたものです。

この4はotherがnodeより後ろにあるという意味で、DOCUMENT_POSITION_FOLLOWINGという定数になっています。この定数は、任意のノードのプロパティとしてJavaScriptから参照できます。だから、

<!doctype html>
<html>
  <head>
    <title>test</title>
  </head>
  <body>
    <p id="p1">p1</p>
    <p id="p2">p2</p>

    <script type="text/javascript">
      var p1 = document.getElementById('p1');
      var p2 = document.getElementById('p2');

      console.log(p1.compareDocumentPosition(p2) == p1.DOCUMENT_POSITION_FOLLOWING );
    </script>
  </body>
</html>

今回は任意のノードとして、当事者(?)であるp1を使用しました。もちろんp2でもいいし他のでもいいです。

この結果がtrueだから、DOCUMENT_POSITION_FOLLOWINGが4であることが分かります。このように、結果と定数を比較することで、そのノードがどのような位置関係であるかが分かります。今回のようにDOCUMENT_POSITION_FOLLOWINGで比較すれば、「otherがnodeの後ろにある」ということがわかります。

定数は、次のものがあります。括弧内の数字は、具体的な数値です。

DOCUMENT_POSITION_PRECEDING
otherはnodeよりにあります。 (2)
DOCUMENT_POSITION_FOLLOWING
otherはnodeよりにあります。 (4)
DOCUMENT_POSITION_CONTAINS
otherはnodeを含んでいます。 (8)
DOCUMENT_POSITION_CONTAINED_BY
otherはnodeに含まれています。 (16)
DOCUMENT_POSITION_DISCONNECTED
otherとnodeは、同じ木構造にありません。 (1)
DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC
位置関係は実装依存です。 (32)

ちなみに、

AAA
|
├―― BBB  ←node
|
└―― CCC
     |
     └―― DDD  ←other
          

のように、nodeとotherが同じ親をもつ兄弟でなくてもよいです。

<AAA><BBB></BBB><CCC><DDD></DDD></CCC></AAA>

のようにタグで見ると、DDDがBBBより後にあるというのが分かりやすいです。

また、DOCUMENT_POSITION_CONTAINS、DOCUMENT_POSITION_CONTAINED_BYの2つは、含む・含まれるという関係です。つまり、

AAA  ←other
|
├―― BBB  ←node
|
└―― CCC
     |
     └―― DDD
          

のような場合、otherはnodeを含んでいます。つまり、DOCUMENT_POSITION_CONTAINSです。逆に、

AAA  ←node
|
├―― BBB  ←other
|
└―― CCC
     |
     └―― DDD
          

のような場合、otherが含まれる側だから、DOCUMENT_POSITION_CONTAINED_BYです。実際にやってみましょう。

<!doctype html>
<html>
  <head>
    <title>test</title>
  </head>
  <body>
    <p id="p1">p1</p>
    <p id="p2">p2</p>

    <script type="text/javascript">
      var p1 = document.getElementById('p1');

      console.log( document.body.compareDocumentPosition(p1) );
    </script>
  </body>
</html>

この場合、

body  ←node
|
├―― p1  ←other
|
└―― p2
          

という関係で、otherがnodeに含まれているから、DOCUMENT_POSITION_CONTAINED_BYの16に当てはまります。

しかし、返ってくる値は20です。20とはなんでしょう。上の定数一覧のなかには20という定数はありません。実は、これは2つの定数が組み合わさった形です。

これは、なぜあの定数が「1,2,3,4,5...」という順番ではなく「1,2,4,8,16,32」というように飛んでいるのかということと関わってきます。

実はこれらの数値は二進数で表すと、「000001(1)」「000010(2)」「000100(4)」「001000(8)」「010000(16)」「100000(32)」というようになっています。それぞれ1桁だけ1になっています。

このような値の特徴は、複数足し合わされてももとの値に分解できるということです。例えば「001101」は、「001000」「000100」「000001」の3つが同時に入った数だといえます。

compareDocumentPositionもこの方式をとっています。今回の20は、二進数だと「010100」です。これは、「010000(16)」「000100(4)」の2つがあるということを表しています。

つまり、DOCUMENT_POSITION_CONTAINED_BYとDOCUMENT_POSITION_FOLLOWINGの両方があてはまる、つまりotherはnodeに含まれていて、かつnodeよりも後ろにあるということです。これがなぜかは、次のようにタグにしてみると分かりやすいです。

<body><p></p> <p></p></body>

p(other)は、body(node)より後ろにありますね。ですから、DOCUMENT_POSITION_CONTAINED_BYだけではなくDOCUMENT_POSITION_FOLLOWINGも成り立ったのです。

ビット演算

さて、プログラムでこれを判定するにはどうすればよいのでしょうか。20は16を含んでいますが、値自体は別なので、==では判定できません。実は、こういうときビット演算を使います。

そのうちのひとつ、ビットごとの論理積は、2つの値を比較し、ビット(2進数の1桁)がどちらも立っている(1である)ビットのみが立った値を返します。例えば、20と16を比較したら、

010100 (20)
010000 (16)
-----------
010000 (16)
          

となり、結果は16です。このように、2つの値で両方が1のビットのみ1になります。

ここで、1,2,4,8,16,32といった定数の値は、ひとつのビットのみが立っている値です。だから、任意の値とこのような値の論理積をとったとき、任意の値のそのビットが立っていれば結果のそのビットが立ち、立っていなければそのビットは立たず結果として全ての桁が立たない(0である)から、0になります

これはif文の判定に利用できます。どこかのビットが立っていれば0ではないからで、立っていなければ0だからとなり、処理を分岐させることが出来ます。

さて、このビットごとの論理積は、JavaScriptでは、2つの値をとって1つの値を返すから、演算子です。&という演算子を使います。まとめると、otherがnodeに含まれているかどうか調べるには、

node.compareDocumentPosition(other) & node.DOCUMENT_POSITION_CONTAINED_BY

これをif文の条件に利用します。

ビットごとの論理和

また、論理積とセットで、ビットごとの論理和というものもあります。論理積が両方とも1のときのみ1になるのに対し、論理和は、片方でも1であれば1になります。例えば、

010000 (16)
000100 (4)
-----------
010100 (20)
          

という感じです。ここで、ビットが1つしか立っていない値どうしの論理和をとると、その両方を表す値ができます。この例はそういう例です。上で「複数の値が足し合わされた」と表現したものは、正確には論理和をとった値であるといえます。このように重複して立つビットがない場合、足すことと論理和をとることは同じ結果になります。演算子は|です。

排他的論理和

もはや今回の内容とは関係ありませんが、ついでにもう1つ紹介しておきます。それは排他的論理和です。これは論理和と似ていますが、排他的論理和は、「片方のみのビットが立っているときその1」という処理を行います。例えば、

011001 (25)
001111 (15)
-----------
010110 (22)
          

両方のビットが立っているときは0になります。演算子は^です。

面白い性質として、ある値との排他的論理和ととった値があるとき、もう一度同じ値で排他的論理和をとると、もとの数値に戻ります。

010110 (22)
001111 (15)
-----------
011001 (25)
          

上の25^15で得られた22で、 もう一度22^15として15と排他的論理和をとると25に戻っています。これは、m^n^nとしたとき、m^(n^n)=m^0=mという性質によるものです。

さて、話をcompareDocumentPositionに戻します。これの結果がある値に当てはまるかを知りたいときは、ビットごとの論理積を使うようにしましょう。

また、DOCUMENT_POSITION_PRECEDING、DOCUMENT_POSITION_FOLLOWINGについても注意が必要です。これが含まれていても、片方がもう片方を含んでいるという可能性があります。そういう場合でもOKという場合ならいいですが、そうでない場合もあると思います。

そういう場合、「DOCUMENT_POSITION_PRECEDINGを含み、かつDOCUMENT_POSITION_CONTAINSを含まない」などといった処理が必要になってくるので、注意しましょう。だから、例えばこうです。

var res = node.compareDocumentPosition(other);
if(res & node.DOCUMENT_POSITION_PRECEDING && !(res & node.DOCUMENT_POSITION_CONTAINS)){
}
          

ここで、よく使うのに今ごろやっと出てきた演算子&&を紹介します。上の&1個のやつと似ていますね。こっちは「論理積」です。1個のほうは「ビットごとの論理積」なので区別しましょう。これは単純で、左右には2つの条件が来ます。これらが2つとも真から真になるという演算子です。つまり、このif文全体では、

res & node.DOCUMENT_POSITION_PRECEDING

!(res & node.DOCUMENT_POSITION_CONTAINS)

が両方真であればこのif文が真になるということです。とてもよく使います。

また、何気なく出てきた演算子!ですが、これは否定演算子で、ある値の前につけて、その値が真なら偽になり、偽なら真になります。つまり、真偽を逆にする演算子です。これは単項演算子の一種です。つまり&などと違って、左右に値があるわけではなく、値は右のみに付けます。

したがって、!(res & node.DOCUMENT_POSITION_CONTAINS)の意味は、「resがnode.DOCUMENT_POSITION_CONTAINSを含まない」ということになりますね。

また、同じようにただの論理和||もあります。これは、条件のどちらか片方が真から真というものです。

さて、他の定数についても説明します。DOCUMENT_POSITION_DISCONNECTEDは、文字通り同じ木構造にない場合です。例えば、createElementで作ったばかりとか、removeChildで木構造から除去されたノードは木構造にありません。そういう場合はこれになります。同じ木構造に属していないので、どちらが前かといった位置関係が定義できないわけです。また、別のdocumentに属するノードでもこうなります。

<!doctype html>
<html>
  <head>
    <title>test</title>
  </head>
  <body>
    <p id="p1">p1</p>
    <p id="p2">p2</p>

    <script type="text/javascript">
      var newp = document.createElement('p');

      console.log( document.body.compareDocumentPosition(newp) );
    </script>
  </body>
</html>

このサンプルでは、木構造上に存在するbody要素と、新しく作ったばかりのp要素を比較しています。返ってくる値は33です(ブラウザによっては1かもしれません)。

33は1と32に分けられるから、DOCUMENT_POSITION_DISCONNECTED(1)がちゃんとあることが分かります。

もうひとつの32は、最後に解説する定数のDOCUMENT_POSITION_IMPLEMENTATION_SPECIFICです。これは「実装依存」ということですが、それはつまり「特に定められていない」ということです。今回の場合、同じ木構造上にないなら当然位置関係なんて分からないし、定められません。だから、この「実装依存」にも当てはまるのです。

今回解説したこのメソッドは便利で、例えば要素がクリックされたときのイベントで、ある要素よりも下の要素で発生した場合のみ処理を行いたい場合などは、このcompareDocumentPositionで判定すれば楽に判定できます。使える機会があれば使ってみましょう。

論理演算子について

最後に、今回登場した|| && !という3つの演算子について少し追加で解説しておきます。これらを文中で「真になる」とか「偽になる」とかいう言い方をしましたが、演算子である以上ある値を返すはずです。これらの返り値は何なんでしょうか。

真・偽といわれると、真偽値を思い出しますね。つまりtrueとfalseです。

!に関してはそれが正しいです。つまり、!aというような式があった場合、aが真なら!aはfalseを返します。逆にaが偽なら!aはtrueを返します。そのため、「aが真ならtrue、偽ならfalse」という処理(真偽値への変換)を行いたい場合に、!!aと書く場合もあります。これは!aの結果をもう一度!で処理するということですね。

ただし、||&&は動作が違います。これらが返すのは真偽値ではありません。これらの動作は次のように決められています。すなわち、a || bは「aが真ならaを返し、aが偽ならbを返す」と決められています。また、a &&は逆に「aが真ならbを返し、aが偽ならaを返す」と決められています。これらをよく考えてみれば、さっき説明したような動作になっていることが分かります。

また、この2つの演算子には短絡実行という特徴があります。これは何かというと、例えばa || bでaが真のときはaを返しますので、bは何でもいいわけです。この場合、bは評価されません。評価されないとはどういうことかというと、計算されないということです。ですから、もしbに関数呼び出しが含まれていたら、aが偽の場合関数呼び出しも行われません。同様に、a && bでaが偽のときもbは評価されません。

短絡実行が役立つ場面は多いです。例えば、簡易的なif文として使われることがあります。

var a=10;
a>5 && console.log(a);

という文は次のif文と同じ意味になります。

var a=10;
if(a>5){
    console.log(a);
}

この場合、&&の文の返り値自体は興味がなくて、その短絡実行という性質を用いているわけです。

また、返り値に興味がある場合も、短絡実行を有効に使える場合があります。例えば、オブジェクトのプロパティを条件判定する場合です。すなわち、

function check(obj){
    if(obj.value>10){
        console.log("valueは10より大きいです");
    }
}

というような関数があった場合、引数にnullを渡したらどうなるでしょうか。これは実は、nullに対してそのプロパティを参照することはできないため、エラーになります。これは回避したいですね。

そのためには、objがnullかどうか判定しなければいけません。objがnullの場合は何もしないことにすれば、次のようにかけます。