JavaScript入門 DOM -DOMインターフェース解説-
JavaScriptを使って画面上の表示内容を取得、変更する方法を解説。Webサイトを作成するときは必ず画面の作成作業が発生します。JavaScriptでどのようにして画面の内容を取得・変更しているか解説します。
DOMインターフェース
JavaScriptからHTMLへの入出力はDOM(Document Object Model)というAPIを通して行います。(DOMは、DOMインターフェースやDOM APIとも呼びます)。JavaScriptのソースコード中では直接HTMLを扱うことができないため、DOMインターフェースを持つDOMオブジェクトを通してHTMLの情報を扱います。
HTMLは、HTML文書とも言います。そのためDOM(Document Object Model)とは、JavaScriptで扱うための、HTML文書のオブジェクトのモデルという意味になります。
DOMインターフェースを持つオブジェクト(DOMオブジェクト)はブラウザの仕様として決められており、開発者はその仕様に沿ってDOMオブジェクトのプロパティの値を変更したり、メソッドを呼び出したりすることでHTMLの表示や変更を行います。なお、HTMLとDOMオブジェクト間の変換はブラウザによって自動的に行われるため、開発者は変換を記述する必要はありません。
DOMツリー
DOMオブジェクトを取得するには、Documentオブジェクトを使用します。「Documentオブジェクトには、HTMLの構造がDOMオブジェクトに変換された状態で、ツリー構造で格納されています。」
この構造をDOMツリーと言います。
また、DOMツリーを構成する個々のオブジェクトは、Node(ノード)と呼びます。Nodeには、テキストやHTMLコメント、またHTMLタグなどの種別があります。
Nodeと言った場合には、HTMLタグ以外のHTMLのコメントやテキスト、タグとタグの間のスペースなどのことも指しますが、Nodeの中でもHTMLタグのみを表す場合には、Element(エレメント)と呼びます。Elementとは、Nodeの種別がElementタイプ(HTML要素)のものを指します。
ElementとNodeは、両方ともコンストラクタによって作成されたオブジェクトです(Elementコンストラクタ、Nodeコンストラクタを継承したコンストラクタから作成されます)。また、ElementはNodeを継承しているため、Nodeオブジェクトで使用可能なプロパティやメソッドはElementオブジェクトからも使用できます。
正確に言うと、DOMツリーには、Elementで構成されたツリーと、Nodeで構成されたツリーの2種類があります。
ElementとNodeのツリーは、それぞれDocumentオブジェクト(document)のchildren、childNodesに格納されています。
- children:Nodeの中でもElementのみ格納されています。
- childNodes:コメントやテキストなどを含むすべての種類のNodeが格納されている
<!DOCTYPE html>
<html>
<head>
<title>タイトル</title>
</head>
<body>
<!-- コメント -->
<h1>見出し</h1>
<p>段落</p>
</body>
</html>
開発ツールのコンソールに直接入力して実行
console.dir( document.children );
childrenプロパティには、HTMLCollectionという配列風(array-like)オブジェクトにElementが格納されています。配列風オブジェクトとは、配列のようなオブジェクトです。特徴としては、配列のように0から始めるインデックスで値を保持ししますが、配列ではないため、配列のメソッドは使用できません。このchildrenにはElementしか格納されないため、DOCTYPEタグや、コメント、テキストはHTMLCollectionには含まれません。また、document.childrenがHTML構造の一番上の階層の要素を格納するため、この中にはhtmlタグに対応するElementが格納されます。
htmlのchildrenを確認
console.dir( document.children[0].children );
このようにElementツリーはDocumentオブジェクト上に保持されます。一方、Nodeツリーの場合は、ChildNodeというプロパティにNodeがツリー上に構築されます。
Nodeツリーの確認
console.dir( document.childNodes);
ChildNodesプロパティには、NodeListという配列風オブジェクトにNodeが格納されています。そのため、HTMLの先頭に記述するDOCTYPEタグもNodeの一種とみなされ、NodeListに格納されています。先のchildrenと同様にNodeツリーの場合は、childNodesに再帰的にNodeが格納されています。
一般的に、DOMインターフェースを使うときには、HTMLタグごとに値の取得・変更を行うため、HTMLタグ(Element)以外のNodeを操作することはありません。そのため、子要素を取得するときにはchildNodesを使うのは非常にまれで、childrenを使って子要素を取得します。
親子関係を表すDOMインターフェース
NodeやElementなどのDOMオブジェクトには、親子関係を持つプロパティがあります。Nodeを取得するためのプロパティ(childNodes)とElementを取得するためのプロパティ(children)などもその1つです。NodeかElementのどちらを取得したいかによって使い分けをします。
プロパティ | 戻り値のタイプ | 説明 |
---|---|---|
parentElement | Element | 親のElementを返す |
parentNode | Node | 親のNodeを返す |
children | HTMLCollection | 子Elementを含む配列風オブジェクトを返す |
childNodes | NodeList | 子Nodeを含む配列風オブジェクトを返す |
firstElementChild | Element | childrenで取得される配列風オブジェクトの最初の要素を返す |
firstChild | Node | ChildNodesで取得される配列風オブジェクトの最初の要素を返す |
lastElementChild | Element | childrenで取得される配列風オブジェクトの最後の要素を返す |
lastChild | Node | ChildNodesで取得される配列風オブジェクトの最後の要素を返す |
previousElementsibling | Element | 自要素と兄弟関係にある1つ前のElementを返す |
previousSibling | Node | 自要素と兄弟関係にある1つ前のNodeを返す |
nextElementSibling | Element | 自要素と兄弟関係にある1つ後のElementを返す |
nextSibling | Node | 自要素と兄弟関係にある1つ後のNodeを返す |
これらのプロパティでは、一致するElementやNodeが見つからない場合には、nullが返ります。また、HTMLCollectionの場合は、空の状態となります。
プロパティを使う場合は、NodeとElementの違いに注意する必要があります。例えば、Elementを取得したいのに、firstChildを使いNodeが返されてしまったり。
基本的にはHTML内のすべてのHTML要素は、DOMツリー上で保持、管理されていますが、一部のHTML要素に関してはDOMツリー外のプロパティからも取得可能です。
プロパティ | 戻り値のタイプ | 説明 |
---|---|---|
document.body | Element | HTML内の<body>のElementを返す |
document.head | Element | HTML内の<head>のElementを返す |
document.images | HTMLCollection | HTML内の<img>のElementを含む配列風オブジェクトを返す |
document.forms | HTMLCollection | HTML内のすべてのElementを含む配列風オブジェクトを返す |
document.embeds | HTMLCollection | HTML内のすべてのElementを含む配列風オブジェクトを返す |
特定のElementを取得DOMインターフェース
DOMインターフェースには、特定のキー情報をもとにしてHTML要素をDOMツリーから取得する方法が用意されています。それらは、Element(オブジェクト)またはDocumentオブジェクト(document)のメソッドとして実装されています。
メソッド | 戻り値 | 説明 |
---|---|---|
getElementById(“idAttr”) | Element|null | idの属性値(idAttr)と一致した最初のElementを取得する。一致する要素がない場合は、nullが返る |
getElementByClassName(“clsAttr”) | HTMLCollection | class属性(clsAttr)を使ってElementが格納された配列風オブジェクトを取得する。一致する要素がない場合は、空のHTMLCollectionが返る |
getElementByName(“nameAttr”) | NodeList | name属性(nameAttr)を使ってElementが格納された配列風オブジェクトを取得する。一致する要素がない場合は、空のHTMLCollectionが返る |
getElementByTagName(“tagName”) | HTMLCollection | タグ(tagName)を使ってElementが格納された配列風オブジェクトを取得する。一致する要素がない場合は、空のHTMLCollectionが返る |
特定のElementを取得するメソッド
<section id="container"> <!--1⃣ -->
<span class="target-cls"></span> <!--2⃣ -->
<div id="list">
<input name="child1"> <!--3⃣ -->
<input name="child2">
</div>
<p class="target-cls"></p> <!--2⃣ 4⃣ -->
</section>
<script>
const elById = document.getElementById( "container" ); //1⃣id属性から要素を取得
const elByCls = document.getElementByClassName( "target-cls" ); //2⃣class属性から要素を取得
const elByName = document.getElementByName( "child1" ); //3⃣name属性から要素を取得
const elByTag = document.getElementByTagName( "p" ); //4⃣タグ名から要素を取得
</script>
セレクタAPIによるElementの取得
現代のブラウザでは、セレクタAPI(Selectors API)を使って、柔軟にElementオブジェクトを取得できます。セレクタAPIは、DocumentオブジェクトまたはElementオブジェクトのメソッドとして提供されています。getElementById、getElementByClassName、getElementByName、getElementByTagNameは、取得する条件によってメソッドを使い分ける必要があるため、便利性に欠けます。それに対して、セレクタAPIは、引数のセレクタ文字列(selector)によって取得条件を指定できるため、現在のJavaScriptでは、基本的にこの方法で取得します。
メソッド | 戻り値 | 説明 |
---|---|---|
querySelector(selector) | Element | セレクタ文字列(selector)に一致した最初のElementを取得する |
querySelectorAll(selector) | NodeList | セレクタ文字列(selector)に一致したすべてのElementを格納した配列風オブジェクトを取得する |
querySelectorメソッドを使った場合は、最初にセレクタ文字列に一致したElementオブジェクトのみ種痘されます。一致したElementオブジェクトをすべて取得したい場合には、querySelectorAllを使ってください。セレクタAPIは、セレクタ文字列とあわせて使います。セレクタ文字列とは、JavaScriptやCSSのコード内でHTML要素を特定するときに使う文字列のことです。
セレクタAPIで使用可能なセレクタ文字列
セレクタ文字列 | 記述例 | 説明 |
---|---|---|
* | * | すべてのタグに一致する |
E | div <div>タグ</div> | タグ名(E)に一致する |
#idAttr | #target <div id = “target”>タグ</div> | idの属性値(idAttr)に一致する |
.clsAttr | .target <div class=”target”>タグ</div> | classの属性値(clsAttr)に一致する |
[attr] | [disabled] <input disabled> | 属性名(attr)に一致する |
[attr=”value”] | [type=”password”] <input type=”password”> | 属性名(attr)の属性値(value)に一致する |
[attr^=”value”] | [href^=”http”] href属性の値がhttpから始まる要素に一致する <a href=”http://example.com/”>リンク</a> | 属性名(attr)の属性値(value)に先頭一致する |
[attr$=”value”] | [href$=”pdf”] href属性の値がpdfで終わる要素に一致する <a href=”/sample.pdf”>PDFリンク</a> | 属性名(attr)の属性値(value)に後方一致する |
[attr*=”value”] | [name*=”text”] name属性の値にtextを含む要素が一致する <input name=”text-1″> 一致 <input name=”text-2″> 一致 | 属性名(attr)の属性値(value)に部分一致する |
S1,S2 | div, .cls 以下のいずれのHTMLにも一致する <div></div> 一致 <p class=”cls”></p> 一致 | セレクタ(S1)またはセレクタ(S2)に一致する。セレクタにおけるOR条件 |
S1S2 | .cls1.cls2 クラス属性にcls1、cls2が付いている要素(<h1>)に一致する <div class=”cls1″> <h1 class=”cls1 cls2″> 一致 <span></span> </h1> </div> | セレクタ(S1)かつセレクタ(S2)に一致する。セレクタにおけるAND条件 |
S1 S2 | div span <div>に含まれる<span>が一致する <div> <h1> <span></span> 一致 </h1> </div> | セレクタ(S1)内のセレクタ(S2)に一致する |
S1 > S2 | div > h1 <div>の子要素の<h1>に一致する <div> <h1></h1> 一致 </div> | セレクタ(S1)の子要素に当たるセレクタ(S2)に一致する |
S1 + S2 | div + h1 <div>の直後の<h1>に一致する <div></div> <h1></h1> 一致 | セレクタ(S1)の直接にあるセレクタ(S2)に一致する |
S1 ~ S2 | div ~ h1 <div>の兄弟、かつ後に位置する<h1>に一致する <div></div> <span></span> <h1></h1> 一致 | セレクタ(S1)の兄弟関係でS1よりも後にあるセレクタ(S2)に一致する |
<section id="container"> <!--1⃣ -->
<span class="target-cls"></span> <!--2⃣ -->
<div id="list">
<input name="child1"> <!--3⃣ -->
<input name="child2">
</div>
<p class="target-cls"></p> <!--2⃣ 4⃣ -->
</section>
<script>
const elById = document.querySelector( "#container" ); //1⃣id属性から要素を取得
const elsByCls = document.querySelectorAll( ".target-cls" ); //2⃣class属性から要素を取得
const elByCls = document.querySelector( ".target-cls" ); //一致した最初の要素のみ取得する場合はquerySelectorを使用
const elByName = document.querySelectorAll( '[name="child1"]' ); //3⃣name属性の値がchild1の要素をすべて取得
const elByTag = document.querySelectorAll( "p" ); //4⃣タグ名に一致するすべての要素を取得
</script>
セレクタAPIは、このような書式で柔軟にHTML要素を取得できます。
getElementByidを使うケース
セレクタAPIの追加以降も、getElementByidメソッドは、querySelectorの代わりに使われることがあります。getElementByidを使ったほうがquerySelectorを使うよりも高速で処理されるためです。これは、getElementByidとquerySelectorの検索アルゴリズムの違いに起因します。この速度の違いは、ページ上に存在するノード数が増えれば増えるほど大きくなります。比較的大きなWebアプリケーション(1ページのノード数が10万のーど程度)になることが想定できるのであれば、id属性の指定にはgetElementByidを使う方がよいでしょう。
祖先要素にさかのぼって検索する
closestメソッドを使うと、親とその親(祖先)と順々とさかのぼって最初に一致する要素(祖先要素と呼びます)を取得できます。
構文:closestメソッド
let closestElement = element.closest( selector );
- closestElement:自要素(element)の祖先要素をさかのぼって検索したときに、最初にセレクタ文字列(selector)に一致するElementを返します。
- element:Elementオブジェクトを設定します。
- selector:セレクタ文字列を設定します。
たとえば、次の例ではsectionタグが2つありますが(❶❷)、❶の<section>の中に#target要素(<span id=”target”></span>)を含んでいます。そのため、#target要素からclosestメソッドで<section>を探しにいったときには、❶の<section>が取得されます。
closestメソッドは祖先要素をさかのぼって検索する
<section style="background-color: yellow;">
これは祖先要素です
<div>
<span id="target"></span>
</div>
</section>
<section style="background-color: orange;">
これは祖先要素ではありません
</section>
<script>
setTimeout(() => {
const terget = document.querySelector( "#target" );
const section = terget.closest( "section" ); //祖先要素のsectionタグを検索
section.prepend( "発見 -> " );
}, 2000); //2秒後に実行
</script>
DOMツリーの構築後にコードを実行
JavaScriptコードからDOMインターフェースを通してHTML要素を取得するときには、JavaScriptコードの実行タイミングに注意する必要があります。そもそもブラウザは、HTMLを上から順番に解説していき、解析が終わったHTMLから順次、DOMツリーを構築していきます。DOMインターフェースを通してHTMLの情報を取得・変更できるのは、DOMツリー上の要素のみです。「JavaScriptコードの実行時に、まだDOMツリー上に読み込まれていないHTMLタグにはアクセスできません。」
DOMツリー上に要素がない場合はエラーになる
<body>
<div id="before">この要素はscriptタグより前にあるため取得可能です。</div>
<script>
const beforeEl = document.querySelector( "#before" );
console.log( beforeEl.textContent );
const afterEl = document.querySelector( "#after" );
console.log( afterEl.textContent );
</script>
<div id="after">この要素はscriptタグより後にあるため取得できません。</div>
</body>
上記のコードでは、scriptタグよりも、#after要素が後に存在するため、<script>の実行タイミングでは#after要素はまだDOMツリー内に配置されていません。そのため、afterElのプロパティ(textContent)にアクセスすると、エラーが発生します。
HTML要素が取得できない場合の対処方法
このエラーを解消する方法次の4つがあります
1、scriptタグをbodyの閉じタグの直前に記述する
<body>の閉じタグ(</body>)の直前に記述することで、すべてのHTMLタグの解析が完了してからJavaScriptコードを実行します。
<html>
<body>
<!-- HTMLタグの記述 -->
<script> /* JavaScriptの実行 */ </script>
</body>
</html>
2、DOMContentloadedイベントまたはloadイベント内でコードを実行する
DOMContentLoadedイベントまたはloadイベント内でJavaScriptコードを実行することで、コードの実行タイミングをDOMツリーの構築後にすることができます。
<body>
<script>
document.addEventListener( "DOMContentLoaded",() => {
/* この関数内のコードはDOMツリー全体の構築が完了してから実行されます */
const afterEl = document.querySelector( "#after" );
console.log( afterEl.textContent );
} );
</script>
<div id="after">scriptタグの後に記載したHTMLタグも取得可能です。</div>
</body>
3、defer属性をScriptタグに付与する
scriptタグにdefer属性を付けることで、DOMツリーの構築後にコードを実行できます。また、deferが付いたscriptタグが複数ある場合には、scriptタグの記述順でコードが実行されます。
同じような属性でasync属性がありますが、scriptタグにasync属性を付けるとDOMツリーの構築前でもJavaScriptコードの読込が完了した時点で実行されます。また、asyncの場合は、scriptタグの記述順に関係なく、読み込みが完了したものから実行されます。
deferの場合
<script src="A.js" defer> //A.js、B.js、C.jsの順番でコードは実行される
<script src="B.js" defer>
<script src="C.js" defer>
<span>この要素を取得可能です。</span> //A.js、B.js、C.jsのすべてのファイルからこの<span>要素は取得可能
asyncの場合
<script src="A.js" async> //A.js、B.js、C.jsは読み込みが完了したものから実行される
<script src="B.js" async>
<script src="C.js" async>
<span>取得可能か不明</span> //A.js、B.js、C.jsは<span>要素の読込前に実行される可能性がある
4、scriptタグにtype=”module”を付加する
<script type=”module”>とした場合には、defer属性を付与したときと同じ挙動になります。