【DOM基礎】ノードの取得／属性の取得・設定

# DOMとは
> Document Object Model (DOM) は、HTML および XML ドキュメントのための API です。これはドキュメントの構造的な表現を提供し、内容や表示形態の変更を可能にします。端的に言えば、Web ページをスクリプトやプログラミング言語とつなぐような機構です。
（DOM | MDNより）

つまりDOM APIを使うことでドキュメントを制御できます。JavaScriptを使えばDOM APIにつなぐことができます。
このことから、JavaScriptとDOMは分かれて扱われています。

## DOMツリー
WebページはDocumentオブジェクトによって表されていて、ブラウザがwebページにアクセスしHTMLを解析すると、**文書の内容を表すオブジェクトのツリー構造**が構築されます。
この構造のことをDOMツリー（またはドキュメントツリー）といいます。

## ノード
DOMツリーを形成する一つ一つのオブジェクトをノード（Node）といいます。

### ノードの種類

例として下記のようなhtmlがあったとします。

```html:dom-tree.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <title>タイトル</title>
    <meta chareset="utf-8">
</head>
<body>
    <h1>大見出し</h1>
    <!-- 本文が入る -->
    <p id="text">テキストテキストテキスト</p>
</body>
</html>
```

おもなノードには下記があります。

| ノードの種類 | 概要 |
|-----------|------------|
|ドキュメントノード |ドキュメント全体を表すDocumentオブジェクト |
| 要素ノード| 要素を表すオブジェクト |
|テキストノード|テキストを表すオブジェクト|
|コメントノード|コメントを表すオブジェクト|
|属性ノード|属性を表すオブジェクト|

上記のdom-tree.htmlをDOMツリーで表すと下記のようになります。

```text: 
document *1
 └ html *2
    ├ head  *2
    │   ├ 空白 *3
    │   ├ title *2 
    │   ├ 空白 *3 
    │   ├ meta *2 
    │   └ 空白 *3 
    ├ 空白 *3
    └ body  *2
        ├ 空白 *3 
        ├ h1 *2 
        │  └ タイトル *3
        ├ 空白 *3 
        ├ コメント *4 
        ├ 空白 *3 
        ├ p *2 
        │  └ テキストテキストテキスト *3
        └ 空白 *3 
        
*1: ドキュメントノード
*2: 要素ノード
*3: テキストノード
*4: コメントノード
```
- ~~通常HTMLでは要素の前後に空白があっても無視されますが、~~DOMツリーでは空白はテキストと扱われます。
(インラインボックスの要素の場合、前後に空白がある場合、1つの半角スペースがあるかのように振舞います[^20170831])
- **空白は空白ノードというテキストノードの一種です**。
- html要素の先頭と末尾には空白ノードは入りません。
- DOMツリーのルートはdocumentオブジェクトです。

### ノードの親子関係
例として、上記のDOMツリーにおけるbodyを基準に考えてみます。
bodyは「h1」の親ノード、「html」の子ノード、「head」の兄弟ノード、「document」の子孫ノード、「タイトル」の祖先ノードと呼びます。
HTML上での呼び方と似たような感じです。

### ノードのプロパティ

| プロパティ名 | 概要 |
|-----------|------------|
|parentNode|親ノードへの参照。（Documentオブジェクトを指定した場合はnull）|
|childNodes|子ノードへの参照を格納するNodeList |
|firstChild|最初の子ノード。（子ノードをもたない場合はnull）|
|lastChild|最後の子ノード。（子ノードをもたない場合はnull）|
|nextSibling|兄弟ノードの中で次のノード。（次の兄弟ノードをもたない場合はnull）|
|previousSibling|兄弟ノードの中で1つ前のノード。（前の兄弟ノードをもたない場合はnull）|
|nodeType|ノードの種類を表す数値。[詳細はこちら](https://developer.mozilla.org/ja/docs/Web/API/Node/nodeType)|
|nodeValue|テキストノードのテキストコンテンツ。（要素ノードやドキュメントノードの場合はnull）|
|nodeName|要素ノードの場合は ~~大文字で~~ HTMLでは大文字、XHTMLでは小文字で要素名。[^20170831]  [詳細はこちら](https://developer.mozilla.org/ja/docs/Web/API/Node/nodeName)|

dom-tree.htmlを例にして、pタグを参照したい場合は下記のコードで参照できます。

```js
console.log(document.lastChild.lastChild.childNodes[5]);
```
ただこの参照では空白ノード（テキストノード）のことを考える必要があり非常に非効率です。
そこで**テキストノードを無視して要素ノードを取り出すためのプロパティ**があります。


| プロパティ名 | 概要 |
|-----------|------------|
|parentElement|親要素ノードへの参照。（親要素ノードがない場合はnull）|
|children|子要素ノードへの参照を格納するHTMLCollection|
|firstElementChild|最初の子要素ノード。（子要素ノードがない場合はnull）|
|lastElementChild|最後の子要素ノード。（子要素ノードがない場合はnull）|
|nextElementSibling|兄弟要素ノードの中で次の要素ノード。（次の兄弟要素ノードをもたない場合はnull）|
|previousElementSibling|兄弟要素ノードの中で1つ前の要素ノード。（前の兄弟要素ノードをもたない場合はnull）|
|childElementCount|子要素ノードの数。（= children.length）|

先ほどのpタグを参照する場合は下記のようになります。

```js
console.log(document.firstElementChild.lastElementChild.children[1]);
```

### ノードオブジェクトの取得
上記のようにdocumentから要素ノードを取得することは不便です。
DOM APIには任意の要素ノードを取得するメソッドがあります。

#### IDから取得

```js
document.getElementById('id名');
```
- 該当するidがない場合はnullが返ります。

#### クラス名から取得

```js
document.getElementsByClassName('クラス名');
```

- 指定したクラスを持つ要素ノードへの参照を格納したHTMLCollectionを返します。
- 複数指定した場合はその複数のクラスを持つ要素ノードのHTMLCollectionを返します。

```html
<ul>
	<li class="color orange">オレンジ</li>
	<li class="color blue">ブルー</li>
	<li class="color green">グリーン</li>
</ul>
```

```js
document.getElementsByClassName('color orange'); // -> [li.color.orange]
```

- **要素ノードから本メソッドの実行が可能**です。

```js
document.getElementById('container').getElementsByClassName('クラス名');
```

#### 要素名から取得
```js
document.getElementsByTagName('要素名');
```
- 指定した要素ノードへの参照を格納したHTMLCollectionを返します。
- ワイルドカード(`*`)の使用が可能です。
- **要素ノードから本メソッドの実行が可能**です。

#### name属性から取得

```js
document.getElementsByName('名前');
```
- 指定した要素ノードへの参照を格納したNodeListを返します。

#### selectors APIから取得
jQueryライクにCSSセレクタを指定して要素を取得できます。

```js
document.querySelector('セレクタ');
document.querySelectorAll('セレクタ');
```
- querySelector()は指定したセレクタにマッチする要素ノードのうち、最初の要素ノードを返します。
- querySelectorAll()は指定したセレクタにマッチする要素ノードへの参照を格納したNodeListを返します。
    - ここでのNodeListはこれまでのメソッドで取得してきたHTMLCollection/NodeListとは違い、**生きていないNodeList**です。詳しくは後述。
- **要素ノードから本メソッドの実行が可能**です。

### 生きているHTMLCollectionと生きていないNodeList
childrenプロパティ、document.getElementsByClassName()、getElementsByTagName()、getElementsByName()メソッドで取得した**HTMLCollection/NodeListは「生きている」動的な状態**です。
これはどういうことかというと、JSでの操作で文書に変更が加えられたら同時に変化するということです。

下記では変数divsは再代入を行っていませんが、中身が変化しているのが分かります。

```js
const divs = document.getElementsByTagName('div');
console.log(divs.length);  // -> 0
const newDiv = document.createElement('div');
document.body.appendChild(newDiv);
console.log(divs.length); // -> 1 変化あり
```

逆に、document.querySelectorAll()メソッドでは**「生きていない」状態のNodeList**です。

```js
const divs = document.querySelectorAll('div');
console.log(divs.length);  // -> 0
const newDiv = document.createElement('div');
document.body.appendChild(newDiv);
console.log(divs.length); // -> 0 変化なし
```

生きている状態をうまく生かすと便利なことがあります。
例えば下記のような動的に追加した要素に、そのままイベントを適用させることが可能です。

[デモページはこちら](https://codepen.io/KDE_SPACE/pen/MvqzOW)

逆にこの特性が邪魔になる場合は、静的なコピーを取ってから処理する必要があります。

- NodeListを返す**node.childNodes**プロパティは、例外的に「生きている」NodeListを返します。
- HTMLCollectionは上述したメソッド以外に下記プロパティから取得できます。
    - document.anchors
    - document.forms
    - document.images
    - document.links
    - node.children

### HTMLCollectionとNodeListのその他の違い
それぞれ独自のプロパティとメソッドを持っています。
**HTMLCollectionでは「id」と「name」から要素の抽出が可能**です。

```html
<div id="div1"></div>
<div class="div2"></div>
<div data-hoge="div3"></div>
<div name="div4"></div>
```

```js
const divs1 = document.getElementsByTagName('div');
const divs2 = document.querySelectorAll('div');

// HTMLCollectionのプロパティを列挙
for (let key in divs1) {
	console.log(key); // -> 0, 1, 2, 3, div1, div4, length, item, namedItem
}
// NodeListのプロパティを列挙
for (let key in divs2) {
	console.log(key); // -> 0, 1, 2, 3, length, item, entries, forEach, keys, values
}

console.log(divs1['div1']) // -> <div id="div1"></div>（ノードオブジェクト）
console.log(divs1['div4']) // -> <div name="div4"></div>（ノードオブジェクト）
```

#### HTMLCollectionとNodeListの見分け方
**constructorプロパティ**を使うのが簡単です。

```js
const divs1 = document.getElementsByTagName('div');
const divs2 = document.querySelectorAll('div');
console.log(divs1.constructor); // -> ƒ HTMLCollection() { [native code] }
console.log(divs2.constructor); // -> ƒ NodeList() { [native code] }
```

しかし@think49さんからのご指摘の通り、constructorプロパティは書き換えが可能なため注意が必要です。[^20170831]
おすすめ頂いた`Object.prototype.toString()`が安定して使えそうです。

```js
const divs1 = document.getElementsByTagName('div');
const divs2 = document.querySelectorAll('div');

divs1.toString = () => 'toStringの中身を置き換える';

console.log(divs1.toString()); // -> 'toStringの中身を置き換える'
console.log(Object.prototype.toString.call(divs1)); // -> [object HTMLCollection]
console.log(Object.prototype.toString.call(divs2)); // -> [object NodeList]
```
上記のように、対象のオブジェクトのtoString()メソッドを上書きされてもcallオブジェクトから呼び出すことでオブジェクトを表す文字列を取得できます。

### ~~DOM Level 0~~ documentプロパティで要素を取得できる
W3CによるDOMの標準化前のレガシーなDOMは~~DOM Level 0と呼ばれています。~~ 正式な仕様ではありませんがDOM Level 0 と呼ばれる場合があります。[^20170831]
通称DOM Level 0ではこれまでのnode.childNodesやdocument.getElementById()などプロパティ、メソッドはありませんでした。

その代わりに要素を取得する方法として下記のようなdocumentプロパティがあり、これは標準化が進められた際にも新たに追加されるなど現在でも使用できるプロパティです。（下記は一部です）

|プロパティ|概要|
|------|------|
|document.documentElement|html要素ノードを参照|
|document.head|head要素ノードを参照|
|document.body|body要素ノードを参照|
|document.forms|form要素ノードへの参照を格納したHTMLCollection|
|document.images|img要素ノードへの参照を格納したHTMLCollection|
|document.links|href属性を持つarea要素とa要素ノードへの参照を格納したHTMLCollection|
|document.scripts|script要素ノードへの参照を格納したHTMLCollection|

## 属性値の取得・設定
属性値は要素ノードのプロパティから、またはメソッドから参照します。

### プロパティを使う
基本的には下記の構文で参照します。

```js
要素ノード.属性名
```
参照後、代入することで設定が可能です。

例）

```html
<p id="text"><a href="http://qiita.com/">Qiita</a></p>
```
```js
const text = document.getElementById('text');
const anchor = text.firstElementChild;

console.log(text.id); // -> text
console.log(anchor.href); // -> http://qiita.com/

anchor.href = 'http://help.qiita.com/';
console.log(anchor.href); // -> http://help.qiita.com/
```

#### 注意点
- 属性名が複数単語の場合、2番目の以降の単語を大文字（lowerCamelCase）にする必要があります。

    例）
    - tabindex -> tabIndex
    - maxlength -> maxLength

- JavaScriptで予約語になっている属性名の場合はプロパティ名の前に「html」を付けます。
例えばlabel要素のfor属性の場合は「htmlFor」になります。
- **class属性は特殊でclassName**になります。（DOM Standardからは**classList**が使えます。[^20170831]）

#### 属性の一覧を取得

```js
element.attributes
```
- 要素ノードに設定されている属性の一覧を**NamedNodeMapオブジェクト**として取得できます。
    - NamedNodeMapオブジェクトは読み出し専用で、属性ノードが配列のような形で格納されています。
    - インデックス番号の他に、属性名でその属性ノードにアクセスできます。
    - 属性ノードではnameプロパティから属性名、valueプロパティから属性値を取得できます。

例）

```html
<div id="hoge" class="fuga" data-sample="piyo"></div>
```
```js
const div = document.getElementById('hoge');
console.log(hoge.attributes);  // -> NamedNodeMap {0: id, 1: class, 2: data-sample, length: 3}
console.log(hoge.attributes[0]); // -> id="hoge" (属性ノード)
console.log(hoge.attributes[1].name); // -> class
console.log(hoge.attributes[2].value); // -> piyo
```

### メソッドを使う
#### 属性値の取得

```js
element.getAttribute('属性名');
```
- **指定した属性名がない場合はnul**l~~もしくは空文字~~が返ります。[^20170901]
- 指定した属性名があり、**属性値が空文字あるいは省略されている場合は空文字**が返ります。[^20170901]
- 属性ノードとして取得したい場合は**getAttributeNode()メソッド**を使います。[^20170903]

#### 属性値の設定

```js
element.setAttribute('属性名', '属性値');
```
- 属性ノードを設定したい場合は**setAttributeNode()メソッド**を使います。[^20170903]


#### 属性があるかないか

```js
element.hasAttribute('属性名');
```

#### 属性の削除

```js
element.removeAttribute('属性名');
```
- 属性ノードとして削除したい場合は**removeAttributeNode()メソッド**を使います。[^20170903]


# 参考
- [徹底マスター JavaScriptの教科書 プログラミングの教養から、言語仕様、開発技法までが正しく身につく (Informatics&IDEA)](https://www.amazon.co.jp/%E5%BE%B9%E5%BA%95%E3%83%9E%E3%82%B9%E3%82%BF%E3%83%BC-JavaScript%E3%81%AE%E6%95%99%E7%A7%91%E6%9B%B8-%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E3%81%AE%E6%95%99%E9%A4%8A%E3%81%8B%E3%82%89%E3%80%81%E8%A8%80%E8%AA%9E%E4%BB%95%E6%A7%98%E3%80%81%E9%96%8B%E7%99%BA%E6%8A%80%E6%B3%95%E3%81%BE%E3%81%A7%E3%81%8C%E6%AD%A3%E3%81%97%E3%81%8F%E8%BA%AB%E3%81%AB%E3%81%A4%E3%81%8F-Informatics-IDEA/dp/4797388641)
- [NodeList - Web API インターフェイス | MDN](https://developer.mozilla.org/ja/docs/Web/API/NodeList)
- [Node.nodeType - Web API インターフェイス | MDN](https://developer.mozilla.org/ja/docs/Web/API/Node/nodeType)
- [Node.nodeName - Web API インターフェイス | MDN](https://developer.mozilla.org/ja/docs/Web/API/Node/nodeName)
- [HTMLCollection - Web API インターフェイス | MDN](https://developer.mozilla.org/ja/docs/Web/API/HTMLCollection)
- [HTMLCollectionオブジェクト | JavaScript プログラミング解説](https://so-zou.jp/web-app/tech/programming/javascript/dom/html-collection.htm)
- [NodeListとHTMLCollectionも別物なので気を付けよう。（DOMおれおれAdvent Calendar 2015 &#8211; 13日目） | Ginpen.com](view-source:https://ginpen.com/2015/12/22/nodelist-vs-htmlcollection/)
- [document - Web API インターフェイス | MDN](https://developer.mozilla.org/ja/docs/Web/API/document)
- [NamedNodeMap - Web API インターフェイス | MDN](https://developer.mozilla.org/ja/docs/Web/API/NamedNodeMap)

[^20170831]: 2017/8/31追記
[^20170901]: 2017/9/1追記
[^20170903]: 2017/9/3追記