uhyohyo.net

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

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

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

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


<!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) ); // 4
    </script>
  </body>
</html>

4が表示されます。この例から分かるように、返り値は数値です。

引数は一つで、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 ); // true
    </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が同じ親をもつ兄弟でなくてもよいです。実際、この木構造をタグで表すと次のようになり、BBBの開始タグはDDDの開始タグより前にあります。

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

このように定義された順序付けを文書順とかtree orderと言います。

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

AAA ←other
  • BBB ←node
  • CCC
    • DDD

のような場合、nodeはotherの子孫なので、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) ); // 20
    </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>p1</p> <p>p2</p></body>

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

ビット演算

さて、20 (010100)という値が与えられたときに、これが例えば16 (010000)を“含む”かどうかを判定するにはどうすればよいのでしょうか。実は、こういうときビット演算を使います。

そのうちのひとつ、ビットごとの論理積は、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」という処理を行います。例えば、25と15の排他的論理は以下の図より22となります。


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^(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)){
  // otherはnodeより前にあり、しかもotherはnodeを含んでいない場合の処理
}

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

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

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

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

ここでは論理積・論理和・否定演算子について「真」とか「偽」とかいう言葉を使って説明しましたが、それはあまり正確ではありませんね。真か偽かというのは、より正確にいえば、真な値というのは真偽値に変換するとtrueになる値のことであり、偽な値というのは真偽値に変換するとfalseになる値ということです。二章第十四回ではif文の条件について真や偽という概念を用いて説明しましたが、実はif文の条件部分に真偽値以外の値が渡されると真偽値に変換されるのです。真偽値への変換については九章第七回で解説します。

ちなみに、JavaScriptである値を真偽値に変換するには、Boolean(値)というようにBoolean関数に渡せばよいです。返り値は当然真偽値です。Boolean(1)trueになる一方Boolean(0)falseになるなどのことを確かめてみましょう。

まず否定演算子!の挙動から説明します。これは簡単で、!の返り値は真偽値です。!は与えられた値を真偽値に変換し、それがtrueならfalseを返し、falseならtrueを返します。

一方、論理積&&と論理和||はより複雑です。実は、A && Bの返り値はAかBかのどちらかです。具体的には、Aを真偽値に変換してtrueならBを返し、falseならAを返します(Aを真偽値に変換した結果を返すわけではないので注意してください)。たとえば、3 && "foo""foo"となります。実はこの動作によって「AとBが両方真なら真な値を返し、そうでなければ偽な値を返す」という性質が成り立ちます。考えてみましょう。

同様に、A || BAを真偽値に変換してtrueならAを返し、falseならBを返します。

また、&&||短絡実行と呼ばれる特徴を持ちます。これは、A && BA || BAを返す場合にはB評価されないということです。評価されないというのは、その部分はプログラムとして実行されないということです。

例えば、fooという関数が存在しない場合、foo()はエラーになります。しかし、true || foo()foo()を含んでいるにも関わらずエラーになりません。これはfoo()の部分は評価されないからです。

さて、他の定数についても説明します。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要素を比較しています。返ってくる値は35か37のどちらかのはずです。

ここでは、この中にDOCUMENT_POSITION_DISCONNECTED(1)が含まれていることが重要です。

最後に残ったDOCUMENT_POSITION_IMPLEMENTATION_SPECIFICは意味としては「実装依存」ということですが、歴史的経緯のため存在しているのでここでは触れません。興味がある人は仕様書等を調べてみると良いかと思います。