DOM(Document Object Model)の概要
DOM(Document Object Model)はHTMLをオブジェクトとして扱うための規格で、HTMLの要素をツリー状の集合として管理します。ツリーを構成するオブジェクトはノードと呼ばれ、Documentオブジェクトをルートとして、HTMLタグやHEADタグ、H1タグやPタグなどがその下の階層としてつながります。ノードは要素を表すだけではなく、属性やテキストなどもノードになります。
DOMはjQueryなどのフレームワークやライブラリと比べると決して扱いやすいとは言えませんが、外部ファイルの読み込みが必要なく、オーバーヘッドの削減にもなります。昔のようにクロスブラウザ問題もほぼなくなっているので、状況に応じてDOMを直接扱うことも選択肢のひとつにできると良いでしょう。
DOMツリー
要素の取得
getElementById()メソッド
今まで何度か登場していますが、getElementById()メソッドを説明します。getElementById()メソッドは要素のIDを指定すると、その要素のオブジェクトを返します。IDがドキュメント内で重複している場合は最初にマッチした要素のオブジェクトを返します。IDがマッチしなかった場合はNullを返します。
構文:
オブジェクト = document.getElementById( id );
getElementById()メソッドで取得したオブジェクトは、innerHTMLプロパティなどを使って変更することができます。
CodePen: https://codepen.io/zionboogie/pen/eVLjbL
HTML
<div id="box1"></div>
JavaScript
// getElementByIdにID名を指定して要素を取得 var e_div1 = document.getElementById( "box1" ); //要素にテキストを追加します。 e_div1.innerHTML = "innerHTMLで文言追加";
innerHTMLは先にあった要素や文字列を上書きするので、追加したい場合は加算代入演算子( += )などを使って追加します。
e_div1.innerHTML += "<p>追加で文言追加</p>";
getElementsByClassName()メソッド
getElementsByClassName()メソッドは指定したクラス名の要素の配列を返します。
構文:
配列 = document.getElementsByClassName( クラス名 );
次のコードではクラス名box2の要素を取得し、その最初の要素に文言を追加しています。
HTML
<div class="box2"></div> <div class="box2"></div>
JavaScript
// getElementsByClassNameにクラス名を指定して要素を取得 var e_div2 = document.getElementsByClassName( "box2" ); // 要素の最初に文言を追加 e_div2[0].innerHTML = "getElementsByClassNameを使って文言追加";
getElementsByClassName()とほぼ同じ使い方ができるメソッドとして、getElementsByTagName()メソッドがあります。getElementsByTagName()メソッドはタグ名を指定して要素を取得します。
querySelector()/querySelectorAll()メソッド
最後にCSSセレクタを使って要素を取得するquerySelector()、querySelectorAll()メソッドを紹介します。querySelector()メソッドはCSSセレクタを使って取得した要素のうち、最初の1つだけを返します。これに対して、querySelectorAll()メソッドはマッチしたすべての要素を返します。
構文:
オブジェクト = document.querySelector( CSSセレクタ ); 配列 = document.querySelectorAll( CSSセレクタ );
querySelector()メソッドの引数には、要素名やID名、クラス名といったCSSセレクタを指定します。要素名にはDIVやPといった要素名をそのまま指定し、IDであればシャープ( # )のあとにID名、クラスであればドット( . )のあとにクラス名を付けます。たとえば先のHTMLを要素名、ID名、クラス名をセレクタで指定する場合、"ul"、"#list01"、".list01"となります。
CodePen: https://codepen.io/zionboogie/pen/BYepoZ
ID名"list01"の要素内にあるクラス名"btn"の要素を指定したい場合、次のように指定します。
HTML
<ul id="list01"> <li><button class="btn">ボタン1</button></li> <li><button class="btn">ボタン2</button></li> </ul>
JavaScript
// CSSセレクタでbtnクラスの要素を指定 var btn = document.querySelector( "#list01 .btn" ); console.log( btn );
出力結果
<button class='btn'>ボタン1</button>
ボタンは2つありますが、querySelector()はマッチした最初の要素を返すので、出力結果でもわかるように取得したのはボタン1つだけです。その他のCSSセレクタにも対応しているので、次のように:last-childなどの指定も可能です。
JavaScript
// CSSセレクタで最後の要素を指定 var li = document.querySelector( "li:last-child" ) console.log( li );
出力結果
<li><button class='btn'>ボタン2</button></li>
指定したセレクタにマッチするすべての要素を取得するには、querySelectorAll()メソッドを使います。次のコードはquerySelectorAll()でLI要素を全て取得し、配列の要素数をlengthプロパティで取得しています。
JavaScript
// LI要素を全て取得 li = document.querySelectorAll( "li" ); // 配列の要素数を出力 console.log( li.length );
要素に追加したい場合はinnerHTMLプロパティを使います。最後の要素のインデックス番号はlengthプロパティを使って取得しています。
JavaScript
// 要素にテキストを追加 li[li.length-1].innerHTML += "追加"
querySelector()、querySelectorAll()は取得後に要素が変更されても、動的に内容が更新されません。また、処理速度も要素を取得するメソッドの中では遅いです。そのため、処理速度が重要な場合や、取得後も最新の要素である必要がある場合は、ID名を指定するgetElementById()などのget系のメソッドを使ってください。
appendChild()メソッド
innerHTML以外にも、要素の内容を変更する手段は他にもあります。appendChild()メソッドは、指定した要素の最後に要素を追加できます。innerHTMLと違い、既存の内容に追加するだけで、内容を上書きしません。
次のコードは、UL要素に新しいLI要素を追加し、LI要素にテキストを追加しています。新しいLI要素の追加には、ノードを作成するcreateElement()メソッド、要素のテキストを設定するtextContentプロパティを使っています。
CodePen: https://codepen.io/zionboogie/pen/OzXJZo
HTML
<ul class="list"> <li>1</li> <li>2</li> <li>3</li> </ul>
JavaScript
// .list要素を取得 var list = document.querySelector( ".list" ); // 挿入用ノードの作成 var li = document.createElement( "li" ); // LI要素のテキストを設定 li.textContent = "4" ; // 作成した要素を.list要素に追加 list.appendChild( li );
クラス名の操作
取得した要素は、classListオブジェクトのメソッドを使ってクラス名の追加や変更ができます。classListは以下のようなメソッドを提供します。
classListが提供するメソッド
メソッド名 | 説明 |
---|---|
add | 要素に指定したクラスを追加します。 |
remove | 要素から指定したクラスを削除します。 |
toggle | 要素に指定したクラスがあれば削除、なければ追加します。 |
contains | 要素に指定したクラスを含むかの論理値を返します。 |
実習|classListを使ってクラスを追加する
次のコードは、UL要素の最初のLI要素のみを取得し、取得した要素のクラス名に"current"を追加します。
CodePen: https://codepen.io/zionboogie/pen/wpGVQM
See the Pen 【04-04-04】クラス名の操作 by jun (@zionboogie) on CodePen.
HTML
<ul class="list"> <li>1</li> <li>2</li> <li>3</li> </ul>
CSS
// 背景色の設定 $bg-color: #00BBF0; // .current要素の背景色を変更 .current{ color: #fff; background-color: $bg-color; } li{ padding: 0.5em; font-size: 1.5em; }
Sassの変数
CSSのコードにある$bg-colorは、Sassが提供するCSS用の変数です。Sassの変数はダラー( $ )を変数名の先頭に付け、コロン(:)で変数名と値を区切ります。
$bg-color: #00BBF0;
変数で定義した値を使いたい際、その変数名を指定します。例えば次のようにbackground-colorの値に$bg-color変数を指定すると、CSSに出力する際に$bg-colorの部分が#00BBF0に変換されます。
background-color: $bg-color;
ここでは1つの設定でしか利用していないのであまり意味がありませんが、基本の色を一通り変数で用意しておき、必要になったときに呼び出すようにしておくと、あとで色変更が必要になったときに変数の値を変更するだけで変数を利用している全ての箇所が自動的に更新されます。もちろん色以外にも、サイズでもパス情報でもなんでも記録できます。文字列の中で変数を使う場合、変数と文字列の区別が付かないとエラーになります。次のコードでは、$url_imgという変数名と画像のファイル名がつながってしまっているので、エラーになります。
$url_img: "/common/images/"; .box { background: url($url_imgmv.png) no-repeat; }
上記のような場合は#{}で変数名を囲んで区別が付くようにできます。
$url_img: "/common/images/"; .box { background: url(#{$url_img}mv.png) no-repeat; }
これでエラーが回避され、次のような出力結果が得られます。
.box { background: url(/common/images/mv.png) no-repeat; }
classList.add()メソッド
次はJavaScriptのコードです。最後の要素を取得するために、:last-child疑似セレクタを使っています。
JavaScript
// LIの最後の要素を取得 var li = document.querySelector( ".list li:last-child" ); // クラス名を追加 li.classList.add( "current" );
出力結果
querySelector()メソッドは、ドット記法を使ってメソッド後にメソッドを複数つなげることができます。これをメソッドチェーンと呼びます。先のコードと同じように、LI要素を取得してクラス名を追加する操作は、次のようにひとつにまとめられます。
document.querySelector( ".list li:last-child" ).classList.add( "current" );
メソッドチェーンに対応していないメソッドもあるので、扱いには注意してください。
複数要素の操作
querySelectorAll()を使った複数要素の操作
前述したようにquerySelectorAll()は複数の要素を取得します。そのため、querySelector()とは違い、各要素にアクセスするための処理が必要になります。そのような場合、【02-05】配列の節でも紹介したforEach()メソッドが便利です。
構文:
配列.forEach( コールバック関数, [オブジェクト] )
forEach()はコールバック関数に配列の要素を一度ずつ渡します。コールバック関数は、3つの引数を受け取ります。第1引数が配列の要素、第2引数が要素のインデックス、第3引数が配列です。
次のコードは複数のLI要素を取得し、forEach()メソッドを使ってそれぞれの要素のテキストを出力しています。
CodePen: https://codepen.io/zionboogie/pen/dJXmzN
HTML
<ul class="list"> <li>1番目の項目</li> <li>2番目の項目</li> <li>3番目の項目</li> </ul>
JavaScript
// LI要素の取得 var items = document.querySelectorAll( "li" ); // 要素毎に処理 items.forEach( function( element, index, obj ){ // コールバック関数の引数を使った出力 console.log( "要素のテキスト:" + element.innerText + " \nインデックス:" + index + " \n要素のテキスト(インデックスを使って取得):" + obj[index].innerText ); } );
出力結果
要素のテキスト:3番目の項目 インデックス:2 要素のテキスト(インデックスを使って取得):3番目の項目
getElementsByClassName()を使った複数要素の操作
getElementsByClassName()は複数要素を取得するので、addEventLisnter()メソッドなどをそのまま使うことができません。querySelectorAll()と同じように繰り返し処理が必要ですが、addEventLisnter()メソッドはforEach()メソッドを持っていないので(利用する方法はありますが)、for分を使います。
CodePen: https://codepen.io/zionboogie/pen/jzwZYz
HTML
<ul> <li class="list">1番目の項目</li> <li class="list">2番目の項目</li> <li class="list">3番目の項目</li> </ul>
JavaScript
// LI要素の取得 var items = document.getElementsByClassName( "list" ); // 要素毎に処理 for( var i = 0; i < items.length; i++ ){ // コールバック関数の引数を使った出力 console.log( "要素のテキスト:" + items[i].innerText + " \nインデックス:" + i ); }
出力結果
要素のテキスト:1番目の項目 インデックス:0 要素のテキスト:2番目の項目 インデックス:1 要素のテキスト:3番目の項目 インデックス:2
CSSスタイルの設定
styleオブジェクトを使ってCSSを変更する
1章でも紹介済みですが、CSSもDOMからアクセスすることができます。CSSのプロパティは要素のstyleオブジェクトに保持され、参照や変更が可能です。CSSのプロパティ名で使われる単語の区切りのハイフン( - )はJavaScriptのプロパティ名としては使えません。ハイフン( - )を削除し、次に続く単語の最初の文字を大文字にします(キャメルケース)。
// 背景色を変更 btn.style.backgroubdColor = 'red';
プロパティなので取得でも同じ構文です。
console.log( btn.style.backgroubdColor );
stylehsheetを使ってCSSを切り替える
styleオブジェクトや、クラスの追加・削除などを使ってスタイルを切り替える方法以外に、LINK要素を使ってスタイルを切り替えることができます。LINK要素で外部スタイルシートファイルを読み込むことができますが、disabled属性を追加することでこのスタイルを無効にすることができます。次のコードは、ボタンを押すたびにdisabled属性を切り替えています。
CodePen: https://codepen.io/zionboogie/pen/xYNXgz
See the Pen 【04-04-06】CSSスタイルの切り替え by jun (@zionboogie) on CodePen.
HTML
<link rel="stylesheet" href="https://codepen.io/zionboogie/pen/MreXPq.css" id="style-change"> <button id="btn-change">スタイルを消す</button>
JavaScript
// ボタン要素を取得し、クリックのイベントリスナーを登録 document.getElementById( "btn-change" ).addEventListener( "click", function(){ // disableのトグル処理 document.getElementById( "style-change" ).disabled = !document.getElementById( "style-change" ).disabled; } );
ブラウザの機能
ブラウザはユーザのリクエスト(要求)をサーバに送信し、サーバが返すレスポンス(応答)を解析してWebページとして表示します。ブラウザを構成する主な要素としてユーザ・インタフェースレイヤー、ネットワークレイヤー、レンダリングエンジン、JavaScriptエンジンがあります。この4つのコンポーネント(部品)の役割は下記の一覧の通りです。
コンポーネント(部品) | 説明 |
---|---|
ユーザ・インタフェース(UI)レイヤー | アドレスバーや戻る・進むボタン、メニューなどのインタフェースです。 |
ネットワークレイヤー | サーバと通信してリソース(資源)をダウンロードし、レンダリングエンジンに渡します。 |
レンダリングエンジン | HTMLやCSSの構文を解析し、DOM要素に変換、コンテンツを表示します。解析中にSCRIPTタグがあった場合、JavaScriptインタプリタに渡します。 |
JavaScriptエンジン | JavaScriptを解析し、実行します。 |
ブラウザのコンポーネント(部品)はそれぞれ連携しながら、リソースの要求からレンダリングまでの処理を行っていきます。その処理の流れを少し詳しく追っていきましょう。
ブラウザはユーザのアクションを受けて、HTTP/HTTPSプロトコルを使ってサーバにHTMLファイルや画像などのリソースをリクエストします。リクエストを受けたサーバ側は指定されたリソースをブラウザに送信します。
ブラウザのネットワークレイヤーはサーバから受け取ったリソースをレンダリングエンジンに渡します。レンダリングエンジンはHTMLドキュメントの上から順番に解析し、DOMノードで構成されたレンダーツリーを作成します。HTMLはこのレンダーツリーを使って線画されます。
ここまでの処理は段階的に一気に行われるのではなく、ある程度解析が済むとその分を線画し、さらに解析、線画が逐次的に繰り返されます。こうすることにより、何も表示されていないない画面の時間を最小にし、体感速度を高めることができます。
次に、JavaScriptの解析と実行を追ってみましょう。レンダリングエンジンはHTMLの解析時にSCRIPTタグに到達すると、SRC属性がなければJavaScriptエンジンにSCRIPTタグ内のコードを渡します。SRC属性に値がある場合はネットワークレイヤーにダウンロードの指示を渡し、読み終えたリソースはJavaScriptエンジンに渡されます。このとき、通常はリソースのダウンロードが終わりJavaScriptが解析されるまで、HTMLの解析は一時的に停止されます。このとき、SCRIPTタグのASYNCやDEFER属性が使われていれば、JavaScriptの解析とHTMLの解析を並行して進められます。