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ツリー

DOMオブジェクトを取得するには、Documentオブジェクトを使用します。「Documentオブジェクトには、HTMLの構造がDOMオブジェクトに変換された状態で、ツリー構造で格納されています。

この構造をDOMツリーと言います。

また、DOMツリーを構成する個々のオブジェクトは、Node(ノード)と呼びます。Nodeには、テキストやHTMLコメント、またHTMLタグなどの種別があります。

Nodeと言った場合には、HTMLタグ以外のHTMLのコメントやテキスト、タグとタグの間のスペースなどのことも指しますが、Nodeの中でもHTMLタグのみを表す場合には、Element(エレメント)と呼びます。Elementとは、Nodeの種別がElementタイプ(HTML要素)のものを指します。

NodeとElementの違い

ElementとNodeは、両方ともコンストラクタによって作成されたオブジェクトです(Elementコンストラクタ、Nodeコンストラクタを継承したコンストラクタから作成されます)。また、ElementはNodeを継承しているため、Nodeオブジェクトで使用可能なプロパティやメソッドはElementオブジェクトからも使用できます。

正確に言うと、DOMツリーには、Elementで構成されたツリーと、Nodeで構成されたツリーの2種類があります。

ElementのツリーとNodeのツリー

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);
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のどちらを取得したいかによって使い分けをします。

プロパティ戻り値のタイプ説明
parentElementElement親のElementを返す
parentNodeNode親のNodeを返す
childrenHTMLCollection子Elementを含む配列風オブジェクトを返す
childNodesNodeList子Nodeを含む配列風オブジェクトを返す
firstElementChildElementchildrenで取得される配列風オブジェクトの最初の要素を返す
firstChildNodeChildNodesで取得される配列風オブジェクトの最初の要素を返す
lastElementChildElementchildrenで取得される配列風オブジェクトの最後の要素を返す
lastChildNodeChildNodesで取得される配列風オブジェクトの最後の要素を返す
previousElementsiblingElement自要素と兄弟関係にある1つ前のElementを返す
previousSiblingNode自要素と兄弟関係にある1つ前のNodeを返す
nextElementSiblingElement自要素と兄弟関係にある1つ後のElementを返す
nextSiblingNode自要素と兄弟関係にある1つ後のNodeを返す

これらのプロパティでは、一致するElementやNodeが見つからない場合には、nullが返ります。また、HTMLCollectionの場合は、空の状態となります。

親子関係を表すプロパティ

プロパティを使う場合は、NodeとElementの違いに注意する必要があります。例えば、Elementを取得したいのに、firstChildを使いNodeが返されてしまったり。

基本的にはHTML内のすべてのHTML要素は、DOMツリー上で保持、管理されていますが、一部のHTML要素に関してはDOMツリー外のプロパティからも取得可能です。

プロパティ戻り値のタイプ説明
document.bodyElementHTML内の<body>のElementを返す
document.headElementHTML内の<head>のElementを返す
document.imagesHTMLCollectionHTML内の<img>のElementを含む配列風オブジェクトを返す
document.formsHTMLCollectionHTML内のすべてのElementを含む配列風オブジェクトを返す
document.embedsHTMLCollectionHTML内のすべてのElementを含む配列風オブジェクトを返す

特定のElementを取得DOMインターフェース

DOMインターフェースには、特定のキー情報をもとにしてHTML要素をDOMツリーから取得する方法が用意されています。それらは、Element(オブジェクト)またはDocumentオブジェクト(document)のメソッドとして実装されています。

メソッド戻り値説明
getElementById(“idAttr”)Element|nullidの属性値(idAttr)と一致した最初のElementを取得する。一致する要素がない場合は、nullが返る
getElementByClassName(“clsAttr”)HTMLCollectionclass属性(clsAttr)を使ってElementが格納された配列風オブジェクトを取得する。一致する要素がない場合は、空のHTMLCollectionが返る
getElementByName(“nameAttr”)NodeListname属性(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で使用可能なセレクタ文字列

セレクタ文字列記述例説明
**すべてのタグに一致する
Ediv
<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,S2div, .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 S2div span
<div>に含まれる<span>が一致する
<div>
    <h1>
        <span></span> 一致
    </h1>
</div>
セレクタ(S1)内のセレクタ(S2)に一致する
S1 > S2div > h1
<div>の子要素の<h1>に一致する
<div>
    <h1></h1> 一致
</div>
セレクタ(S1)の子要素に当たるセレクタ(S2)に一致する
S1 + S2div + h1
<div>の直後の<h1>に一致する
<div></div>
<h1></h1> 一致
セレクタ(S1)の直接にあるセレクタ(S2)に一致する
S1 ~ S2div ~ 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属性を付与したときと同じ挙動になります。