第3版が公開されました。こちらでご覧ください

第13章
ドキュメントオブジェクトモデル

ブラウザでウェブページを開くと、ブラウザはページのHTMLテキストを取得して解析します。これは第11章のパーサーがプログラムを解析するのとよく似ています。ブラウザはドキュメントの構造のモデルを作成し、このモデルを使用して画面にページを描画します。

このドキュメントの表現は、JavaScriptプログラムがサンドボックス内で利用できるおもちゃの1つです。モデルを読み取ることも、変更することもできます。これはライブデータ構造として機能します。変更されると、画面上のページが変更を反映するように更新されます。

ドキュメント構造

HTMLドキュメントを、入れ子になった一連のボックスとして想像できます。<body></body>などのタグは、他のタグを囲み、そのタグがさらに他のタグやテキストを含みます。以下は前の章からのドキュメント例です。

<!doctype html>
<html>
  <head>
    <title>My home page</title>
  </head>
  <body>
    <h1>My home page</h1>
    <p>Hello, I am Marijn and this is my home page.</p>
    <p>I also wrote a book! Read it
      <a href="https://eloquentjavascript.dokyumento.jp">here</a>.</p>
  </body>
</html>

このページの構造は次のとおりです。

HTML document as nested boxes

ブラウザがドキュメントを表現するために使用するデータ構造はこの形状に従います。各ボックスに対してオブジェクトがあり、これを使ってHTMLタグが表すものや、それが含むボックスとテキストなどの情報を調べることができます。この表現はドキュメントオブジェクトモデル、略してDOMと呼ばれます。

グローバル変数documentを使用すると、これらのオブジェクトにアクセスできます。そのdocumentElementプロパティは、<html>タグを表すオブジェクトを参照します。また、それらの要素のオブジェクトを保持するheadbodyプロパティも提供します。

木構造

第11章の構文木を思い出してください。それらの構造は、ブラウザのドキュメントの構造と驚くほど似ています。各ノードは、他のノードであるを参照でき、それらの子もまた独自の子を持つことができます。この形状は、要素がそれ自体に似たサブ要素を含めることができる入れ子構造に典型的です。

分岐構造を持ち、サイクル(ノードは直接または間接的にそれ自体を含むことはできない)がなく、単一の明確な「ルート」を持つデータ構造をと呼びます。DOMの場合、document.documentElementがルートとして機能します。

木構造はコンピュータサイエンスでよく登場します。HTMLドキュメントやプログラムなどの再帰的な構造を表すことに加えて、要素が通常、ソートされたフラット配列よりもソートされた木構造でより効率的に見つけられたり挿入されたりするため、ソートされたデータのセットを維持するためによく使用されます。

典型的な木構造には、さまざまな種類のノードがあります。Egg言語の構文木には、変数、値、およびアプリケーションノードがありました。アプリケーションノードには常に子がありますが、変数と値はリーフ、つまり子を持たないノードです。

DOMも同様です。HTMLタグを表す通常の要素のノードが、ドキュメントの構造を決定します。これらは子ノードを持つことができます。そのようなノードの例はdocument.bodyです。これらの子の一部は、テキストやコメント(コメントはHTMLで<!---->の間に記述される)などのリーフノードになる可能性があります。

各DOMノードオブジェクトには、ノードのタイプを識別する数値コードを含むnodeTypeプロパティがあります。通常の要素は、定数プロパティdocument.ELEMENT_NODEとしても定義されている値1を持ちます。ドキュメント内のテキストセクションを表すテキストノードは、値3(document.TEXT_NODE)を持ちます。コメントは値8(document.COMMENT_NODE)を持ちます。

したがって、ドキュメントツリーを視覚化する別の方法は次のとおりです。

HTML document as a tree

リーフはテキストノードであり、矢印はノード間の親子関係を示しています。

標準

ノードタイプを表すために暗号化された数値コードを使用することは、JavaScriptのようなやり方ではありません。この章の後半で、DOMインターフェイスの他の部分も扱いにくく異質に感じられることがわかるでしょう。これは、DOMがJavaScript専用に設計されたものではないためです。むしろ、HTMLだけでなく、HTMLのような構文を持つ汎用データ形式であるXMLなど、他のシステムでも使用できる言語中立のインターフェイスを定義しようとしています。

これは残念なことです。標準は多くの場合役立ちます。しかし、この場合、利点(言語間の整合性)はそれほど説得力があるわけではありません。使用している言語と適切に統合されたインターフェイスを用意することで、言語間で使い慣れたインターフェイスを用意するよりも多くの時間を節約できます。

そのような統合の不十分な例として、DOM内の要素ノードが持つchildNodesプロパティを考えてみましょう。このプロパティは、lengthプロパティと子ノードにアクセスするための数値でラベル付けされたプロパティを持つ配列のようなオブジェクトを保持します。ただし、これは実際の配列ではなくNodeList型のインスタンスであるため、sliceforEachなどのメソッドはありません。

そして、単に設計が不十分な問題もあります。たとえば、新しいノードを作成して、すぐに子や属性を追加する方法はありません。代わりに、まず作成し、次に子を1つずつ追加し、最後に副作用を使用して属性を1つずつ設定する必要があります。DOMを頻繁に操作するコードは、長くて繰り返しが多く、醜くなる傾向があります。

しかし、これらの欠陥は致命的ではありません。JavaScriptでは独自の抽象化を作成できるため、実行している操作をより明確かつ短く表現できるヘルパー関数を簡単に記述できます。実際、ブラウザプログラミングを目的とした多くのライブラリには、そのようなツールが付属しています。

ツリー内を移動する

DOMノードには、近くの他のノードへの豊富なリンクが含まれています。次の図はこれらを示しています。

Links between DOM nodes

図は各タイプのリンクを1つしか示していませんが、すべてのノードには、それを含むノードを指すparentNodeプロパティがあります。同様に、すべての要素ノード(ノードタイプ1)には、その子を保持する配列のようなオブジェクトを指すchildNodesプロパティがあります。

理論的には、これらの親と子のリンクだけを使用して、ツリー内のどこにでも移動できます。しかし、JavaScriptでは、便利な追加のリンクにもアクセスできます。firstChildプロパティとlastChildプロパティは、最初と最後の子要素を指し、子を持たないノードの場合は値nullを持ちます。同様に、previousSiblingnextSiblingは、隣接するノードを指します。これらは、同じ親を持ち、ノード自体の直前または直後に表示されるノードです。最初の子の場合、previousSiblingはnullになり、最後の子の場合、nextSiblingはnullになります。

このような入れ子になったデータ構造を扱う場合、再帰関数が役立つことがよくあります。次の再帰関数は、特定の文字列を含むテキストノードをドキュメント全体でスキャンし、見つかった場合はtrueを返します。

function talksAbout(node, string) {
  if (node.nodeType == document.ELEMENT_NODE) {
    for (var i = 0; i < node.childNodes.length; i++) {
      if (talksAbout(node.childNodes[i], string))
        return true;
    }
    return false;
  } else if (node.nodeType == document.TEXT_NODE) {
    return node.nodeValue.indexOf(string) > -1;
  }
}

console.log(talksAbout(document.body, "book"));
// → true

テキストノードのnodeValueプロパティは、それが表すテキスト文字列を参照します。

要素の検索

親、子、兄弟間のこれらのリンクをナビゲートすることは、前の関数のようにドキュメント全体を走査する場合によく役立ちます。しかし、ドキュメント内の特定のノードを見つけたい場合、document.bodyから開始して、ハードコードされたリンクのパスを盲目的にたどって到達するのは悪い考えです。そうすると、ドキュメントの正確な構造について、後で変更する可能性のある前提条件をプログラムに埋め込んでしまうことになります。もう1つの複雑な要因は、ノード間の空白に対してもテキストノードが作成されることです。例のドキュメントのbodyタグには、3つ(<h1>と2つの<p>要素)の子があるだけでなく、実際には7つあります。つまり、それら3つに加えて、前、後、およびそれらの間の空白です。

したがって、そのドキュメントのリンクのhref属性を取得したい場合、「ドキュメントのbodyの6番目の子の2番目の子を取得する」のようなことは言いたくありません。「ドキュメントの最初のリンクを取得する」と言えるほうが良いでしょう。そして、それができます。

var link = document.body.getElementsByTagName("a")[0];
console.log(link.href);

すべての要素ノードにはgetElementsByTagNameメソッドがあり、これは指定されたノードの子孫(直接または間接の子)である、指定されたタグ名を持つすべての要素を収集し、それらを配列のようなオブジェクトとして返します。

特定の単一のノードを見つけるには、id属性を指定して、代わりにdocument.getElementByIdを使用できます。

<p>My ostrich Gertrude:</p>
<p><img id="gertrude" src="img/ostrich.png"></p>

<script>
  var ostrich = document.getElementById("gertrude");
  console.log(ostrich.src);
</script>

3番目の同様のメソッドは、getElementsByClassNameです。これはgetElementsByTagNameと同様に、要素ノードの内容を検索し、class属性に指定された文字列を持つすべての要素を取得します。

ドキュメントの変更

DOMデータ構造のほとんどすべてを変更できます。要素ノードには、その内容を変更するために使用できる多くのメソッドがあります。removeChildメソッドは、ドキュメントから指定された子ノードを削除します。子を追加するには、appendChildを使用できます。これは、子リストの末尾に配置します。または、insertBeforeを使用することもできます。これは、最初引数として指定されたノードを2番目の引数として指定されたノードの前に挿入します。

<p>One</p>
<p>Two</p>
<p>Three</p>

<script>
  var paragraphs = document.body.getElementsByTagName("p");
  document.body.insertBefore(paragraphs[2], paragraphs[0]);
</script>

ノードは、ドキュメント内の1つの場所にのみ存在できます。したがって、「3つ目」の段落を「1つ目」の段落の前に挿入すると、まずドキュメントの末尾から削除され、次に先頭に挿入されるため、「3つ目/1つ目/2つ目」になります。ノードをどこかに挿入するすべての操作は、副作用として、現在の位置から(もしあれば)削除されます。

replaceChildメソッドは、子ノードを別のノードと置き換えるために使用されます。これには、新しいノードと置き換えるノードの2つのノードが引数として渡されます。置換されるノードは、メソッドが呼び出される要素の子である必要があります。replaceChildinsertBeforeは両方とも、最初の引数として新しいノードを期待することに注意してください。

ノードの作成

次の例では、ドキュメント内のすべての画像(<img>タグ)を、画像の代替テキスト表現を指定するalt属性に保持されているテキストに置き換えるスクリプトを作成します。

これには、画像の削除だけでなく、それらを置き換えるための新しいテキストノードの追加も含まれます。このために、document.createTextNodeメソッドを使用します。

<p>The <img src="img/cat.png" alt="Cat"> in the
  <img src="img/hat.png" alt="Hat">.</p>

<p><button onclick="replaceImages()">Replace</button></p>

<script>
  function replaceImages() {
    var images = document.body.getElementsByTagName("img");
    for (var i = images.length - 1; i >= 0; i--) {
      var image = images[i];
      if (image.alt) {
        var text = document.createTextNode(image.alt);
        image.parentNode.replaceChild(text, image);
      }
    }
  }
</script>

文字列を指定すると、createTextNodeはタイプ3 DOMノード(テキストノード)を提供します。これはドキュメントに挿入して画面に表示させることができます。

画像を処理するループは、ノードリストの末尾から開始されます。これは、getElementsByTagNameのようなメソッド(またはchildNodesのようなプロパティ)によって返されるノードリストがライブであるためです。つまり、ドキュメントが変更されると更新されます。もし先頭から開始した場合、最初の画像を削除するとリストの最初の要素が失われ、2回目のループ(iが1の場合)では、コレクションの長さも1になっているため、ループが停止します。

ライブなコレクションではなく、固定されたノードのコレクションが必要な場合は、コレクションに対して配列のsliceメソッドを呼び出すことで、コレクションを実際の配列に変換できます。

var arrayish = {0: "one", 1: "two", length: 2};
var real = Array.prototype.slice.call(arrayish, 0);
real.forEach(function(elt) { console.log(elt); });
// → one
//   two

通常の要素ノード(タイプ1)を作成するには、document.createElementメソッドを使用できます。このメソッドはタグ名を受け取り、指定されたタイプの新しい空のノードを返します。

次の例では、要素ノードを作成し、残りの引数をそのノードの子として扱うユーティリティeltを定義します。この関数は、引用に簡単な属性を追加するために使用されます。

<blockquote id="quote">
  No book can ever be finished. While working on it we learn
  just enough to find it immature the moment we turn away
  from it.
</blockquote>

<script>
  function elt(type) {
    var node = document.createElement(type);
    for (var i = 1; i < arguments.length; i++) {
      var child = arguments[i];
      if (typeof child == "string")
        child = document.createTextNode(child);
      node.appendChild(child);
    }
    return node;
  }

  document.getElementById("quote").appendChild(
    elt("footer", "—",
        elt("strong", "Karl Popper"),
        ", preface to the second editon of ",
        elt("em", "The Open Society and Its Enemies"),
        ", 1950"));
</script>

属性

リンクのhrefなど、一部の要素属性は、要素のDOMオブジェクトの同じ名前のプロパティからアクセスできます。これは、一般的に使用される標準属性の限られたセットの場合です。

しかし、HTMLでは、ノードに任意の属性を設定できます。これにより、ドキュメントに余分な情報を保存できるため便利です。ただし、独自に属性名を考えた場合、それらの属性は要素のノードのプロパティとしては存在しません。代わりに、getAttributeおよびsetAttributeメソッドを使用してそれらを操作する必要があります。

<p data-classified="secret">The launch code is 00000000.</p>
<p data-classified="unclassified">I have two feet.</p>

<script>
  var paras = document.body.getElementsByTagName("p");
  Array.prototype.forEach.call(paras, function(para) {
    if (para.getAttribute("data-classified") == "secret")
      para.parentNode.removeChild(para);
  });
</script>

このような独自の属性の名前には、他の属性と競合しないようにdata-をプレフィックスとして付けることをお勧めします。

簡単な例として、data-language属性を持つ<pre>タグ(コードや同様のプレーンテキストに使用される「整形済み」)を探し、その言語のキーワードを粗雑に強調表示する「シンタックスハイライター」を作成します。

function highlightCode(node, keywords) {
  var text = node.textContent;
  node.textContent = ""; // Clear the node

  var match, pos = 0;
  while (match = keywords.exec(text)) {
    var before = text.slice(pos, match.index);
    node.appendChild(document.createTextNode(before));
    var strong = document.createElement("strong");
    strong.appendChild(document.createTextNode(match[0]));
    node.appendChild(strong);
    pos = keywords.lastIndex;
  }
  var after = text.slice(pos);
  node.appendChild(document.createTextNode(after));
}

関数highlightCodeは、<pre>ノードと、要素に含まれるプログラミング言語のキーワードに一致する正規表現(「グローバル」オプションがオン)を受け取ります。

textContentプロパティを使用してノード内のすべてのテキストを取得し、それを空の文字列に設定します。これにより、ノードが空になります。キーワード式の一致すべてをループ処理し、一致したテキストの間のテキストを通常のテキストノードとして追加し、一致したテキスト(キーワード)を<strong>(太字)要素でラップされたテキストノードとして追加します。

data-language属性を持つすべての<pre>要素をループ処理し、言語の正しい正規表現を使用してそれぞれにhighlightCodeを呼び出すことで、ページ上のすべてのプログラムを自動的に強調表示できます。

var languages = {
  javascript: /\b(function|return|var)\b/g /* … etc */
};

function highlightAllCode() {
  var pres = document.body.getElementsByTagName("pre");
  for (var i = 0; i < pres.length; i++) {
    var pre = pres[i];
    var lang = pre.getAttribute("data-language");
    if (languages.hasOwnProperty(lang))
      highlightCode(pre, languages[lang]);
  }
}

以下に例を示します

<p>Here it is, the identity function:</p>
<pre data-language="javascript">
function id(x) { return x; }
</pre>

<script>highlightAllCode();</script>

JavaScript言語で予約語である、よく使用される属性が1つあります。それはclassです。歴史的な理由(一部の古いJavaScriptの実装では、キーワードまたは予約語に一致するプロパティ名を処理できなかったため)から、この属性にアクセスするために使用されるプロパティはclassNameと呼ばれます。getAttributeおよびsetAttributeメソッドを使用すると、実際の名前である"class"でもアクセスできます。

レイアウト

要素の種類によって、表示されるレイアウトが異なることに気付いたかもしれません。段落(<p>)や見出し(<h1>)などの一部の要素は、ドキュメントの幅全体を使用し、別の行にレンダリングされます。これらはブロック要素と呼ばれます。リンク(<a>)や、前の例で使用した<strong>要素などの他の要素は、周囲のテキストと同じ行にレンダリングされます。このような要素はインライン要素と呼ばれます。

任意のドキュメントに対して、ブラウザはレイアウトを計算できます。これにより、要素のタイプとコンテンツに基づいて各要素のサイズと位置が決まります。このレイアウトは、実際にドキュメントを描画するために使用されます。

要素のサイズと位置は、JavaScriptからアクセスできます。offsetWidthoffsetHeightプロパティは、要素が占有するスペースをピクセル単位で示します。ピクセルはブラウザの基本的な測定単位であり、通常は画面に表示できる最小のドットに対応します。同様に、clientWidthclientHeightは、ボーダーの幅を無視して、要素のスペースのサイズを示します。

<p style="border: 3px solid red">
  I'm boxed in
</p>

<script>
  var para = document.body.getElementsByTagName("p")[0];
  console.log("clientHeight:", para.clientHeight);
  console.log("offsetHeight:", para.offsetHeight);
</script>

画面上の要素の正確な位置を見つける最も効果的な方法は、getBoundingClientRectメソッドです。これは、topbottomleft、およびrightプロパティを持つオブジェクトを返します。これらのプロパティは、要素の辺のピクセル位置を画面の左上を基準にして示します。ドキュメント全体を基準にする場合は、グローバルなpageXOffsetおよびpageYOffset変数にある現在のスクロール位置を追加する必要があります。

ドキュメントのレイアウトを行うのはかなりの作業になることがあります。速度を考慮して、ブラウザエンジンは変更されるたびにドキュメントを即座に再レイアウトするのではなく、できる限り長く待機します。ドキュメントを変更したJavaScriptプログラムの実行が完了すると、ブラウザは変更されたドキュメントを画面に表示するために新しいレイアウトを計算する必要があります。プログラムがoffsetHeightのようなプロパティを読み取ったり、getBoundingClientRectを呼び出すことによって何かの位置やサイズを要求する場合、正しい情報を提供するためにもレイアウトを計算する必要があります。

DOMレイアウト情報の読み取りとDOMの変更を交互に繰り返すプログラムは、多くのレイアウトの実行を強制し、その結果として非常に遅く実行されます。次のコードは、この例を示しています。これには、幅2000ピクセルのX文字の行を作成し、それぞれにかかる時間を測定する2つの異なるプログラムが含まれています。

<p><span id="one"></span></p>
<p><span id="two"></span></p>

<script>
  function time(name, action) {
    var start = Date.now(); // Current time in milliseconds
    action();
    console.log(name, "took", Date.now() - start, "ms");
  }

  time("naive", function() {
    var target = document.getElementById("one");
    while (target.offsetWidth < 2000)
      target.appendChild(document.createTextNode("X"));
  });
  // → naive took 32 ms

  time("clever", function() {
    var target = document.getElementById("two");
    target.appendChild(document.createTextNode("XXXXX"));
    var total = Math.ceil(2000 / (target.offsetWidth / 5));
    for (var i = 5; i < total; i++)
      target.appendChild(document.createTextNode("X"));
  });
  // → clever took 1 ms
</script>

スタイリング

HTML要素の種類によって異なる動作が表示されることを確認しました。ブロックとして表示されるものもあれば、インラインで表示されるものもあります。コンテンツを太字にする<strong>や、青色で下線を引く<a>のように、スタイルを追加するものもあります。

<img>タグが画像を表示する方法や、<a>タグをクリックするとリンクがたどられるようにするのは、要素のタイプに強く結び付けられています。ただし、テキストの色や下線など、要素に関連付けられているデフォルトのスタイルは変更できます。以下は、styleプロパティを使用した例です

<p><a href=".">Normal link</a></p>
<p><a href="." style="color: green">Green link</a></p>

style属性には、プロパティ(colorなど)の後にコロンと値(greenなど)が続く1つ以上の宣言を含めることができます。複数の宣言がある場合は、"color: red; border: none"のようにセミコロンで区切る必要があります。

スタイリングによって影響を受ける可能性のある側面はたくさんあります。たとえば、displayプロパティは、要素をブロックとして表示するか、インライン要素として表示するかを制御します。

This text is displayed <strong>inline</strong>,
<strong style="display: block">as a block</strong>, and
<strong style="display: none">not at all</strong>.

blockタグは、ブロック要素が周囲のテキストとインラインで表示されないため、独自の行に表示されます。最後のタグはまったく表示されません—display: noneは、要素が画面に表示されないようにします。これは要素を非表示にする方法です。ドキュメントから完全に削除するよりも、後で再度表示するのが簡単になるため、望ましいことが多いです。

JavaScriptコードは、ノードのstyleプロパティを通じて、要素のスタイルを直接操作できます。このプロパティは、すべての可能なスタイルプロパティのプロパティを持つオブジェクトを保持します。これらのプロパティの値は文字列であり、要素のスタイルの特定の側面を変更するために書き込むことができます。

<p id="para" style="color: purple">
  Pretty text
</p>

<script>
  var para = document.getElementById("para");
  console.log(para.style.color);
  para.style.color = "magenta";
</script>

font-familyなど、一部のスタイルプロパティ名にはダッシュが含まれています。このようなプロパティ名はJavaScriptでは扱いにくいため(style["font-family"]のように記述する必要があります)、このようなプロパティのstyleオブジェクトのプロパティ名にはダッシュが削除され、その後の文字が大文字になります(style.fontFamily)。

カスケードスタイル

HTMLのスタイリングシステムは、Cascading Style Sheetsの略であるCSSと呼ばれます。スタイルシートとは、ドキュメント内の要素をスタイル設定する方法のルールセットです。これは、<style>タグ内に記述できます。

<style>
  strong {
    font-style: italic;
    color: gray;
  }
</style>
<p>Now <strong>strong text</strong> is italic and gray.</p>

名前のカスケードとは、複数のルールが結合されて、要素の最終的なスタイルが生成されるという事実を指します。前の例では、font-weight: boldを与える<strong>タグのデフォルトのスタイリングが、<style>タグのルールによって上書きされ、font-stylecolorが追加されます。

複数のルールが同じプロパティの値を定義する場合、最後に読み取られたルールがより高い優先順位を獲得し、勝者になります。したがって、<style>タグのルールに、デフォルトのfont-weightルールと競合するfont-weight: normalが含まれている場合、テキストは太字ではなく通常のテキストになります。ノードに直接適用されるstyle属性のスタイルは、最も高い優先順位を持ち、常に優先されます。

CSSルールでは、タグ名以外を対象にすることも可能です。.abcのルールは、class属性に"abc"を持つすべての要素に適用されます。#xyzのルールは、id属性が"xyz"である要素(ドキュメント内で一意である必要があります)に適用されます。

.subtle {
  color: gray;
  font-size: 80%;
}
#header {
  background: blue;
  color: white;
}
/* p elements, with classes a and b, and id main */
p.a.b#main {
  margin-bottom: 20px;
}

最後に定義されたルールが優先されるという規則は、ルールが同じ特異度を持つ場合にのみ当てはまります。ルールの特異度とは、マッチする要素をどれだけ正確に記述しているかの尺度であり、要素の側面(タグ、クラス、またはID)の数と種類によって決定されます。例えば、p.aを対象とするルールは、pや単に.aを対象とするルールよりも特異度が高く、そのためそれらよりも優先されます。

p > a {…}という表記は、<p>タグの直接の子であるすべての<a>タグに、指定されたスタイルを適用します。同様に、p a {…}は、直接の子であろうと間接的な子であろうと、<p>タグ内のすべての<a>タグに適用されます。

クエリセレクター

この本では、スタイルシートをそれほど頻繁には使用しません。スタイルシートを理解することはブラウザでのプログラミングに不可欠ですが、スタイルシートがサポートするすべてのプロパティとそれらのプロパティ間の相互作用を適切に説明するには、2、3冊の本が必要になります。

私がセレクター構文(スタイルシートで使用される、どの要素にスタイルのセットを適用するかを決定するための表記)を紹介した主な理由は、この同じミニ言語をDOM要素を見つけるための効果的な方法として使用できるからです。

documentオブジェクトと要素ノードの両方で定義されているquerySelectorAllメソッドは、セレクタ文字列を受け取り、それに一致するすべての要素を含む配列のようなオブジェクトを返します。

<p>And if you go chasing
  <span class="animal">rabbits</span></p>
<p>And you know you're going to fall</p>
<p>Tell 'em a <span class="character">hookah smoking
  <span class="animal">caterpillar</span></span></p>
<p>Has given you the call</p>

<script>
  function count(selector) {
    return document.querySelectorAll(selector).length;
  }
  console.log(count("p"));           // All <p> elements
  // → 4
  console.log(count(".animal"));     // Class animal
  // → 2
  console.log(count("p .animal"));   // Animal inside of <p>
  // → 2
  console.log(count("p > .animal")); // Direct child of <p>
  // → 1
</script>

getElementsByTagNameのようなメソッドとは異なり、querySelectorAllによって返されるオブジェクトはライブではありません。ドキュメントを変更しても変更されません。

querySelectorメソッド(Allの部分がないもの)も同様に機能します。これは、特定の単一の要素が必要な場合に便利です。最初に一致する要素のみを返し、一致する要素がない場合はnullを返します。

位置決めとアニメーション

positionスタイルプロパティは、レイアウトに強力な影響を与えます。デフォルトではstaticの値を持っており、これは要素がドキュメント内の通常の位置に配置されることを意味します。relativeに設定すると、要素はドキュメント内でスペースを占有し続けますが、topおよびleftスタイルプロパティを使用して、通常の位置を基準に移動できるようになります。positionabsoluteに設定すると、要素は通常のドキュメントフローから削除されます。つまり、スペースを占有しなくなり、他の要素と重なる可能性があります。また、topおよびleftプロパティを使用して、positionプロパティがstaticではない最も近い囲み要素の左上隅を基準に、またはそのような囲み要素が存在しない場合はドキュメントを基準に絶対的に配置できます。

これを使ってアニメーションを作成できます。次のドキュメントは、楕円を描いて浮遊する猫の画像を表示します。

<p style="text-align: center">
  <img src="img/cat.png" style="position: relative">
</p>
<script>
  var cat = document.querySelector("img");
  var angle = 0, lastTime = null;
  function animate(time) {
    if (lastTime != null)
      angle += (time - lastTime) * 0.001;
    lastTime = time;
    cat.style.top = (Math.sin(angle) * 20) + "px";
    cat.style.left = (Math.cos(angle) * 200) + "px";
    requestAnimationFrame(animate);
  }
  requestAnimationFrame(animate);
</script>

画像はページの中央に配置され、positionrelativeに設定されています。その画像を移動するために、画像のtopleftスタイルを繰り返し更新します。

スクリプトは、ブラウザが画面を再描画する準備ができたときにanimate関数を実行するようにスケジュールするためにrequestAnimationFrameを使用します。animate関数自体も、次の更新をスケジュールするためにrequestAnimationFrameを呼び出します。ブラウザのウィンドウ(またはタブ)がアクティブな場合、これにより、1秒あたり約60回の頻度で更新が行われ、見栄えの良いアニメーションが生成される傾向があります。

ループ内でDOMを更新しただけでは、ページがフリーズし、画面に何も表示されません。ブラウザは、JavaScriptプログラムが実行されている間は表示を更新せず、ページとの対話も許可しません。これがrequestAnimationFrameが必要な理由です。これにより、ブラウザは現在完了したことを認識し、画面の更新やユーザーアクションへの応答など、ブラウザが行うことを進めることができます。

私たちのアニメーション関数には、現在の時間が引数として渡され、それを以前に確認した時間(lastTime変数)と比較して、猫の1ミリ秒あたりの動きが安定し、アニメーションがスムーズに動くようにします。ステップごとに固定量だけ移動した場合、例えば同じコンピューターで実行されている別の重いタスクによって関数がほんの一瞬実行されなくなった場合、動きが途切れ途切れになる可能性があります。

円運動は、三角関数Math.cosMath.sinを使用して行います。これらの関数に慣れていない方のために、この本で時々必要になるため、簡単に紹介します。

Math.cosMath.sinは、半径1単位で点(0,0)を中心とする円上に存在する点を見つけるのに役立ちます。両方の関数は、引数を円上の位置として解釈します。ゼロは円の右端の点を示し、2π(約6.28)まで時計回りに進むと、円全体を一周します。Math.cosは、円の周りの指定された位置に対応する点のx座標を示し、Math.sinはy座標を返します。2πより大きい、または0より小さい位置(または角度)は有効です。回転は繰り返されるため、a+2πはaと同じ角度を指します。

Using cosine and sine to compute coordinates

猫のアニメーションコードは、アニメーションの現在の角度のカウンターであるangleを保持し、animate関数が呼び出されるたびに、経過時間に応じて増加させます。そして、この角度を使用して、画像要素の現在の位置を計算できます。topスタイルはMath.sinで計算され、20を掛けていますが、これは円の垂直半径です。leftスタイルはMath.cosに基づいており、200を掛けているため、円は高さよりもはるかに幅広くなり、楕円運動になります。

スタイルには通常、単位が必要です。この場合、ブラウザにピクセル数(センチメートル、「ems」、またはその他の単位とは対照的に)を数えていることを伝えるために、数値に"px"を追加する必要があります。これは忘れがちです。単位なしで数値を使用すると、数値が0の場合を除き、スタイルは無視されます。0は常に同じ意味を持ち、単位に関係ありません。

まとめ

JavaScriptプログラムは、DOMと呼ばれるデータ構造を通じて、ブラウザが表示している現在のドキュメントを検査および操作することができます。このデータ構造は、ドキュメントのブラウザモデルを表し、JavaScriptプログラムはそれを変更して、表示されているドキュメントを変更できます。

DOMは、ドキュメントの構造に従って要素が階層的に配置されるツリーのように構成されています。要素を表すオブジェクトには、parentNodechildNodesなどのプロパティがあり、これらを使用してこのツリーをナビゲートできます。

ドキュメントの表示方法は、ノードに直接スタイルを適用することと、特定のノードに一致するルールを定義することの両方で、スタイリングの影響を受ける可能性があります。colordisplayなど、さまざまなスタイルプロパティがあります。JavaScriptは、要素のstyleプロパティを通じて、要素のスタイルを直接操作できます。

演習

テーブルを作成する

第6章でプレーンテキストのテーブルを作成しました。HTMLを使用すると、テーブルのレイアウトがはるかに簡単になります。HTMLテーブルは、次のタグ構造で構築されます

<table>
  <tr>
    <th>name</th>
    <th>height</th>
    <th>country</th>
  </tr>
  <tr>
    <td>Kilimanjaro</td>
    <td>5895</td>
    <td>Tanzania</td>
  </tr>
</table>

に対して、<table>タグには<tr>タグが含まれています。これらの<tr>タグの中に、セル要素を配置できます。見出しセル(<th>)または通常のセル(<td>)のいずれかです。

第6章で使用したのと同じソースデータは、サンドボックスのMOUNTAINS変数でも利用できます。また、ウェブサイトからダウンロードすることもできます。

すべてのオブジェクトが同じプロパティのセットを持つオブジェクトの配列が与えられた場合、テーブルを表すDOM構造を構築する関数buildTableを作成します。テーブルには、<th>要素でラップされたプロパティ名を持つヘッダー行と、配列内のオブジェクトごとに1つの後続行があり、そのプロパティ値は<td>要素に含まれている必要があります。

オブジェクトが持つプロパティ名を含む配列を返す関数Object.keysがここで役立つでしょう。

基本が機能したら、数値を含むセルでstyle.textAlignプロパティを"right"に設定して、右揃えにします。

<style>
  /* Defines a cleaner look for tables */
  table  { border-collapse: collapse; }
  td, th { border: 1px solid black; padding: 3px 8px; }
  th     { text-align: left; }
</style>

<script>
  function buildTable(data) {
    // Your code here.
  }

  document.body.appendChild(buildTable(MOUNTAINS));
</script>

新しい要素ノードを作成するにはdocument.createElementを使用し、テキストノードを作成するにはdocument.createTextNodeを使用し、ノードを他のノードに入れるにはappendChildメソッドを使用します。

キー名を1回ループして上部の行を埋め、次に配列内の各オブジェクトに対して再度ループしてデータ行を構築する必要があります。

関数の最後に、囲みである<table>要素を返すことを忘れないでください。

タグ名による要素

getElementsByTagNameメソッドは、指定されたタグ名のすべての子要素を返します。ノードと文字列(タグ名)を引数として取り、指定されたタグ名のすべての子孫要素ノードを含む配列を返す、メソッドではない通常の関数として独自に実装してください。

要素のタグ名を見つけるには、そのtagNameプロパティを使用します。ただし、これはすべて大文字でタグ名を返すことに注意してください。これを補うために、文字列メソッドtoLowerCaseまたはtoUpperCaseを使用してください。

<h1>Heading with a <span>span</span> element.</h1>
<p>A paragraph with <span>one</span>, <span>two</span>
  spans.</p>

<script>
  function byTagName(node, tagName) {
    // Your code here.
  }

  console.log(byTagName(document.body, "h1").length);
  // → 1
  console.log(byTagName(document.body, "span").length);
  // → 3
  var para = document.querySelector("p");
  console.log(byTagName(para, "span").length);
  // → 2
</script>

この解決策は、この章で先に定義したtalksAbout関数と同様に、再帰関数で最も簡単に表現できます。

byTagname自体を再帰的に呼び出し、結果の配列を連結して出力を作成することもできます。より効率的なアプローチとしては、外側の関数で定義された配列変数にアクセスできる、自身を再帰的に呼び出す内部関数を定義し、見つかった一致する要素を追加することができます。外側の関数から内部関数を一度呼び出すことを忘れないでください。

再帰関数はノードタイプをチェックする必要があります。ここでは、ノードタイプ1(document.ELEMENT_NODE)のみに関心があります。このようなノードについては、その子要素をループ処理し、各子要素について、クエリに一致するかどうかを確認すると同時に、その子要素自身を検査するために再帰呼び出しを行う必要があります。

猫の帽子

以前に定義した猫のアニメーションを拡張して、猫と彼の帽子(<img src="img/hat.png">)の両方が楕円の反対側を公転するようにします。

または、帽子を猫の周りを回るようにするか、アニメーションを他の興味深い方法で変更してください。

複数のオブジェクトの配置を容易にするために、絶対位置指定に切り替えるのが良いでしょう。これは、topleftがドキュメントの左上を基準にカウントされることを意味します。負の座標の使用を避けるために、位置の値に固定されたピクセル数を加算するだけで済みます。

<img src="img/cat.png" id="cat" style="position: absolute">
<img src="img/hat.png" id="hat" style="position: absolute">

<script>
  var cat = document.querySelector("#cat");
  var hat = document.querySelector("#hat");
  // Your code here.
</script>