吉本集の個人ブログ
Web制作の技術について書いています。たまに日記も書きます。

【JS入門】DOMについて Vol.1(DOM、インターフェース)

2018年2月3日 / category : javascript

今回からDOM(Document Object Model)について触れていこうと思います。聞いたことのある言葉だと思いますが、DOMとはHTML、SVG、XML文書をプログラムで扱うための仕様書(プログラミング・インターフェース)です。プログラム言語ではありません。
ブラウザにはそれぞれ異なるレンダリングエンジンを搭載していますが、基本的なフローは同じです。

ブラウザにURLを入力しアクセスすると、該当のWebサイトのページ(HTML文書)が読み込まれます。
ブラウザに搭載されているレンダリングエンジンは、HTML文書を解析し、DOMの仕様に沿ってDOMツリーを作ります。

HTML文書の解析(パーサー)

例えば、次のようなHTMLがあったとします。

index.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>タイトル</title>
</head>
<body>
<p id="txt">お腹減った</p>
<script src="./js/sample.js"></script>
</body>
</html>

レンダリングエンジンがHTML文書をツリー状に解析します。これを「解析ツリー」「構文ツリー」「レンダリングツリー」などと呼ばれています。

DOMツリー

続いて、上の解析ツリーをDOMの仕様に沿ってDOMオブジェクトに変換していきます。この時に、それぞれの要素を外部の言語(JavaScriptなど)のインターフェースをオブジェクトとして実装します。基本的にはDOMと要素(マークアップ)は対になっています。

上のようなツリー状の構造をDOMツリーと呼びます。

ノード(Node)とは

HTML文書の要素(マークアップ)はオブジェクトとして実装されますが、細かくオブジェクト化されます。

これらのオブジェクトの集合をノードと呼び、それぞれ要素ノード、属性ノード、テキストノードと呼びます。

レンダリングエンジンがDOMツリーを構築します。DOMツリー状にあるDOMオブジェクトをJavaScriptが操作することになります。それでは実際にコードで確かめてみます。DOMツリーの先頭にあるのがdocumentオブジェクトです。このオブジェクトを調べてみます。

継承されているインターフェース

sample.js
console.log( document ); // #document

コンソール画面

#document
	<!DOCTYPE html>
	<html lang="ja">
		<head></head>
		<body></body>
	</html>

展開するとHTML文書が表示されました。このdocumentオブジェクトがどんなデータなのかを調べてみます。

sample.js
console.log( typeof document ); // object
console.log( document.__proto__ ); // HTMLDocument {...}
console.log( document.constructor ); // ƒ HTMLDocument() { [native code] }
console.log( document instanceof HTMLDocument ); // true

documentオブジェクトはHTMLDocumentインターフェースを継承して実装されたDOMオブジェクトということが確認できました。
ここで少しインターフェースについて触れます。レンダリングエンジンによって要素がインターフェースを継承したオブジェクトとして実装されますが、インターフェースとは、定数やメソッドを定義したオブジェクトであるが、具体的な実装を含んでいません。また、基本的にはコンストラクタ関数を実行することができません。
HTMLDocumentインターフェースのコンストラクタ関数を実行してみます。

sample.js
let d = new HTMLDocument(); // TypeError: Illegal constructor

上のコードで、コンストラクタ関数を実行すると、エラーになることが確認できました。
基本的にDOMオブジェクトはdocumentオブジェクトが持つメソッドcreateElementで生成します。
今回はDOMについての解説のためメソッドについての解説は割愛します。次回以降の記事で改めて解説します。
続けて、html要素を調べてみます。

sample.js
let collection = document.getElementsByTagName('html');
console.log( collection ); // HTMLCollection [html]

let html = collection[0];
console.log( html ); // <html lang="ja">
console.log( html.__proto__ ); // HTMLHtmlElement {...}

documentオブジェクトのメソッドgetElementsByTagNameでHTML上にあるhtml要素を全て参照し、HTMLCollectionインターフェースを継承したオブジェクトとして変数collectionに代入しています。
HTMLCollectionインターフェースは、Arrayオブジェクトを継承していませんが、配列と似た扱いができます。
HTML上にhtml要素は1つですので、最初の値(データ)を変数htmlに代入しています。
この変数htmlを確認すると、HTMLHtmlElementインターフェースを継承したオブジェクトということがわかります。
続いて、p要素を見てみます。

sample.js
let p = document.getElementById('txt');
console.log( p ); // <p id="txt">お腹減った</p>
console.log( p.__proto__ ); // HTMLParagraphElement {...}

p要素にはid属性にtxtを設定していますので、documentオブジェクトのメソッドgetElementByIdを使用してp要素を取得できます。取得したオブジェクトを調べると、HTMLParagraphElementインターフェースを継承したオブジェクトだということがわかります。そのほかの要素も同様に調べると次のようになっています。

それではこれらのインターフェースをさらに見ていきます。

sample.js
console.log( HTMLDocument.__proto__ ); // ƒ Document() { [native code] }
console.log( HTMLHtmlElement.__proto__ ); // ƒ HTMLElement() { [native code] }
console.log( HTMLHeadElement.__proto__ ); // ƒ HTMLElement() { [native code] }
console.log( HTMLMetaElement.__proto__ ); // ƒ HTMLElement() { [native code] }
console.log( HTMLTitleElement.__proto__ ); // ƒ HTMLElement() { [native code] }
console.log( HTMLBodyElement.__proto__ ); // ƒ HTMLElement() { [native code] }
console.log( HTMLParagraphElement.__proto__ ); // ƒ HTMLElement() { [native code] }
console.log( HTMLScriptElement.__proto__ ); // ƒ HTMLElement() { [native code] }

HTMLDocumentインターフェースは、Documentインターフェースを継承し、その他のHTML上の要素は、HTMLElementインターフェースを継承しています。さらに見ていきます。

sample.js
console.log( Document.__proto__ ); // ƒ Node() { [native code] }
console.log( HTMLElement.__proto__ ); // ƒ Element() { [native code] }

Documentインターフェースは、Nodeインターフェースを継承し、HTMLElementインターフェースは、Elementインターフェースを継承しています。さらに見ていきます。

sample.js
console.log( Node.__proto__ ); // ƒ EventTarget() { [native code] }
console.log( Element.__proto__ ); // ƒ Node() { [native code] }

Nodeインターフェースは、EventTargetインターフェースを継承し、Elementインターフェースは、Nodeインターフェースを継承しています。さらに見ていきます。

sample.js
console.log( EventTarget.__proto__ ); // ƒ () { [native code] }
console.log( EventTarget.__proto__ === Function.prototype ); // true

EventTargetインターフェースのプロトタイプを見るとFunctionオブジェクトのプロトタイプを継承していることがわかりました。EventTargetインターフェースがインターフェースの継承の終着点のようです。このEventTargetインターフェースは、今後解説する予定のイベントを持たせられるインターフェースです。ユーザーはHTML上にあるボタンをクリックすることができます。この「クリックする」というのは、EventTargetインターフェースが実装されているイベントの機能になります。また、ウィンドウを「リサイズする」「スクロールする」などもイベントです。これらはトップレベルであるWindowオブジェクトがEventTargetインターフェースを継承しているため実装ができます。

sample.js
console.log( Window.__proto__ ); // ƒ EventTarget() { [native code] }

ここまでを先ほどの図に追加してまとめてみます。

これらのインターフェースの型を調べるとどれもfunction(Object型)になります。過去の記事「オブジェクトについて Vol.4(プロトタイプチェーン、コンストラクタ)」で触れてしますが、関数定義によるオブジェクトは全てFunctionオブジェクトのプロトタイプを経由しています。