第13章ドキュメントオブジェクトモデル
ブラウザでウェブページを開くと、ブラウザはページのHTMLテキストを取得して解析します。これは第11章のパーサーがプログラムを解析するのとよく似ています。ブラウザはドキュメントの構造のモデルを作成し、このモデルを使用して画面にページを描画します。
このドキュメントの表現は、JavaScriptプログラムがサンドボックス内で利用できるおもちゃの1つです。モデルを読み取ることも、変更することもできます。これはライブデータ構造として機能します。変更されると、画面上のページが変更を反映するように更新されます。
ドキュメント構造
HTMLドキュメントを、入れ子になった一連のボックスとして想像できます。<body>
や</body>
などのタグは、他のタグを囲み、そのタグがさらに他のタグやテキストを含みます。以下は前の章からのドキュメント例です。
<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タグが表すものや、それが含むボックスとテキストなどの情報を調べることができます。この表現はドキュメントオブジェクトモデル、略してDOMと呼ばれます。
グローバル変数document
を使用すると、これらのオブジェクトにアクセスできます。そのdocumentElement
プロパティは、<html>
タグを表すオブジェクトを参照します。また、それらの要素のオブジェクトを保持するhead
とbody
プロパティも提供します。
木構造
第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
)を持ちます。
したがって、ドキュメントツリーを視覚化する別の方法は次のとおりです。
リーフはテキストノードであり、矢印はノード間の親子関係を示しています。
標準
ノードタイプを表すために暗号化された数値コードを使用することは、JavaScriptのようなやり方ではありません。この章の後半で、DOMインターフェイスの他の部分も扱いにくく異質に感じられることがわかるでしょう。これは、DOMがJavaScript専用に設計されたものではないためです。むしろ、HTMLだけでなく、HTMLのような構文を持つ汎用データ形式であるXMLなど、他のシステムでも使用できる言語中立のインターフェイスを定義しようとしています。
これは残念なことです。標準は多くの場合役立ちます。しかし、この場合、利点(言語間の整合性)はそれほど説得力があるわけではありません。使用している言語と適切に統合されたインターフェイスを用意することで、言語間で使い慣れたインターフェイスを用意するよりも多くの時間を節約できます。
そのような統合の不十分な例として、DOM内の要素ノードが持つchildNodes
プロパティを考えてみましょう。このプロパティは、length
プロパティと子ノードにアクセスするための数値でラベル付けされたプロパティを持つ配列のようなオブジェクトを保持します。ただし、これは実際の配列ではなくNodeList
型のインスタンスであるため、slice
やforEach
などのメソッドはありません。
そして、単に設計が不十分な問題もあります。たとえば、新しいノードを作成して、すぐに子や属性を追加する方法はありません。代わりに、まず作成し、次に子を1つずつ追加し、最後に副作用を使用して属性を1つずつ設定する必要があります。DOMを頻繁に操作するコードは、長くて繰り返しが多く、醜くなる傾向があります。
しかし、これらの欠陥は致命的ではありません。JavaScriptでは独自の抽象化を作成できるため、実行している操作をより明確かつ短く表現できるヘルパー関数を簡単に記述できます。実際、ブラウザプログラミングを目的とした多くのライブラリには、そのようなツールが付属しています。
ツリー内を移動する
DOMノードには、近くの他のノードへの豊富なリンクが含まれています。次の図はこれらを示しています。
図は各タイプのリンクを1つしか示していませんが、すべてのノードには、それを含むノードを指すparentNode
プロパティがあります。同様に、すべての要素ノード(ノードタイプ1)には、その子を保持する配列のようなオブジェクトを指すchildNodes
プロパティがあります。
理論的には、これらの親と子のリンクだけを使用して、ツリー内のどこにでも移動できます。しかし、JavaScriptでは、便利な追加のリンクにもアクセスできます。firstChild
プロパティとlastChild
プロパティは、最初と最後の子要素を指し、子を持たないノードの場合は値null
を持ちます。同様に、previousSibling
とnextSibling
は、隣接するノードを指します。これらは、同じ親を持ち、ノード自体の直前または直後に表示されるノードです。最初の子の場合、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つのノードが引数として渡されます。置換されるノードは、メソッドが呼び出される要素の子である必要があります。replaceChild
とinsertBefore
は両方とも、最初の引数として新しいノードを期待することに注意してください。
ノードの作成
次の例では、ドキュメント内のすべての画像(<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からアクセスできます。offsetWidth
とoffsetHeight
プロパティは、要素が占有するスペースをピクセル単位で示します。ピクセルはブラウザの基本的な測定単位であり、通常は画面に表示できる最小のドットに対応します。同様に、clientWidth
とclientHeight
は、ボーダーの幅を無視して、要素内のスペースのサイズを示します。
<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
メソッドです。これは、top
、bottom
、left
、および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-style
とcolor
が追加されます。
複数のルールが同じプロパティの値を定義する場合、最後に読み取られたルールがより高い優先順位を獲得し、勝者になります。したがって、<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
スタイルプロパティを使用して、通常の位置を基準に移動できるようになります。position
をabsolute
に設定すると、要素は通常のドキュメントフローから削除されます。つまり、スペースを占有しなくなり、他の要素と重なる可能性があります。また、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>
画像はページの中央に配置され、position
がrelative
に設定されています。その画像を移動するために、画像のtop
とleft
スタイルを繰り返し更新します。
スクリプトは、ブラウザが画面を再描画する準備ができたときにanimate
関数を実行するようにスケジュールするためにrequestAnimationFrame
を使用します。animate
関数自体も、次の更新をスケジュールするためにrequestAnimationFrame
を呼び出します。ブラウザのウィンドウ(またはタブ)がアクティブな場合、これにより、1秒あたり約60回の頻度で更新が行われ、見栄えの良いアニメーションが生成される傾向があります。
ループ内でDOMを更新しただけでは、ページがフリーズし、画面に何も表示されません。ブラウザは、JavaScriptプログラムが実行されている間は表示を更新せず、ページとの対話も許可しません。これがrequestAnimationFrame
が必要な理由です。これにより、ブラウザは現在完了したことを認識し、画面の更新やユーザーアクションへの応答など、ブラウザが行うことを進めることができます。
私たちのアニメーション関数には、現在の時間が引数として渡され、それを以前に確認した時間(lastTime
変数)と比較して、猫の1ミリ秒あたりの動きが安定し、アニメーションがスムーズに動くようにします。ステップごとに固定量だけ移動した場合、例えば同じコンピューターで実行されている別の重いタスクによって関数がほんの一瞬実行されなくなった場合、動きが途切れ途切れになる可能性があります。
円運動は、三角関数Math.cos
とMath.sin
を使用して行います。これらの関数に慣れていない方のために、この本で時々必要になるため、簡単に紹介します。
Math.cos
とMath.sin
は、半径1単位で点(0,0)を中心とする円上に存在する点を見つけるのに役立ちます。両方の関数は、引数を円上の位置として解釈します。ゼロは円の右端の点を示し、2π(約6.28)まで時計回りに進むと、円全体を一周します。Math.cos
は、円の周りの指定された位置に対応する点のx座標を示し、Math.sin
はy座標を返します。2πより大きい、または0より小さい位置(または角度)は有効です。回転は繰り返されるため、a+2πはaと同じ角度を指します。
猫のアニメーションコードは、アニメーションの現在の角度のカウンターであるangle
を保持し、animate
関数が呼び出されるたびに、経過時間に応じて増加させます。そして、この角度を使用して、画像要素の現在の位置を計算できます。top
スタイルはMath.sin
で計算され、20を掛けていますが、これは円の垂直半径です。left
スタイルはMath.cos
に基づいており、200を掛けているため、円は高さよりもはるかに幅広くなり、楕円運動になります。
スタイルには通常、単位が必要です。この場合、ブラウザにピクセル数(センチメートル、「ems」、またはその他の単位とは対照的に)を数えていることを伝えるために、数値に"px"
を追加する必要があります。これは忘れがちです。単位なしで数値を使用すると、数値が0の場合を除き、スタイルは無視されます。0は常に同じ意味を持ち、単位に関係ありません。
まとめ
JavaScriptプログラムは、DOMと呼ばれるデータ構造を通じて、ブラウザが表示している現在のドキュメントを検査および操作することができます。このデータ構造は、ドキュメントのブラウザモデルを表し、JavaScriptプログラムはそれを変更して、表示されているドキュメントを変更できます。
DOMは、ドキュメントの構造に従って要素が階層的に配置されるツリーのように構成されています。要素を表すオブジェクトには、parentNode
やchildNodes
などのプロパティがあり、これらを使用してこのツリーをナビゲートできます。
ドキュメントの表示方法は、ノードに直接スタイルを適用することと、特定のノードに一致するルールを定義することの両方で、スタイリングの影響を受ける可能性があります。color
やdisplay
など、さまざまなスタイルプロパティがあります。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>
タグ名による要素
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">
)の両方が楕円の反対側を公転するようにします。
または、帽子を猫の周りを回るようにするか、アニメーションを他の興味深い方法で変更してください。
複数のオブジェクトの配置を容易にするために、絶対位置指定に切り替えるのが良いでしょう。これは、top
とleft
がドキュメントの左上を基準にカウントされることを意味します。負の座標の使用を避けるために、位置の値に固定されたピクセル数を加算するだけで済みます。
<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>