【DOM基礎】要素内容の取得・設定／ノードの作成・挿入・削除

[前回](http://qiita.com/KDE_SPACE/items/e21bb31dd4d9c162c4a6)の続きです。

# 要素内容の取得・設定

## innerHTML
要素内のHTMLを取得・設定できます。

```js
// 取得
const html = element.innerHTML;

// 設定
element.innerHTML = '<h1>innerHTMLプロパティでHTMLコードの設定が可能です</h1>';

// 空文字を入れることで要素内を空にできる
element.innerHTML = '';
```

- 言わずもがなよく使われるプロパティですが、HTMLコードを文字列として扱うため複雑なコードの編集にはやや不向きなところがあります。
- 取得する際に対象の要素内容が空の場合、空文字が返ります。
- 空文字を入れることで要素内のノードを削除できます。削除についてはremoveChild()を使っても可能です。

## textContent
webページを表示した際の要素内容のテキスト内容を表します。

```js
// 取得
const text = element.textContent;

// 設定
element.textContent = '要素のテキストを設定します';

// 空文字を入れることで要素内を空にできる
element.textContent = '';
```

- 取得する際は、指定したノードのすべての子孫ノードのtextContent属性を連結した値が返ります。  
  また対象の要素内容が空の場合、空文字が返ります。
- 選択したノードのこのプロパティに値を設定すると、そのすべての子孫ノードが取り除かれて、指定した値のテキストノードに置き換わります。
- HTMLを値として渡した場合、`&`、`<`、`>`などのHTML特殊文字はエスケープされます。そのためセキュリティ的にも要素内のテキストを取得・設定したい場合はinnerHTMLではなくtextContentを使うのが望ましいです。

```html
<div id="target1"><p>Hello, <strong>world!</strong></p></div>
<div id="target2"></div>
```

```js
const target1 = document.getElementById('target1');
console.log(target1.textContent); // -> 'Hello, world!'
target.textContent = '<span>置き換え</span>'; // -> ブラウザ上では「<span>置き換え</span>」と表示される
target.textContent = ''; // -> HTMLは右記になる： <div id="target"></div>

const target2 = document.getElementById('target2');
console.log(document.textContent); // -> ''
```

### innerTextとの違い
**textContentプロパティはIE9以上**から使用できます。**IE8以下の場合はinnerTextプロパティ**で代用することができます。
textContentとinnerTextとの違いは下記（一部）です。

- textContentは`<script>`タグの中を返しますが, innerTextは返しません。
- textContentは非表示の要素のテキストを返しますが、innetTextは返しません。

```html
<div id="target"><div style="display:none;">非表示要素のテキスト</div></div>
```
```js
const target = document.getElementById('target');
console.log(target.textContent); // -> '非表示要素のテキスト'
console.log(target.innerText); // -> ''
```
- textContentは空白文字をそのまま返しますが、innerTextは余分な空白を削除して返します。

# ノードの作成・挿入・削除
## ノードの作成

ノードの種類によって下記メソッドで新たなノードが作成できます。

| メソッド | 作成するノード |
|---|---|
| document.createElement(要素名) | 要素ノード |
| document.createAttribute(属性名)  | 属性ノード |
| document.createTextNode(テキスト)  | テキストノード |
| document.createComment(テキスト)  | コメントノード |
| document.createDocumentFragment()  | ドキュメントの断片　[詳しくはこちら](#ノードのdomへの挿入は毎回再描画されるのでcreatedocumentfragmentを利用する) |
| node.cloneNode(deep)  | ノードの複製。deepをtrueにするとnodeの子孫ノードまで複製し、falseの場合はnodeのみを複製する |

- これらのメソッドで作成された直後のノードはメモリ上に存在しているだけでDOMツリーとは関係がありません。

例として下記のようなDOMツリーを作成したいと思います。

```html:目指すDOMツリー
<div id="container">
	<!-- 下記は段落要素 -->
	<p data-foo="bar">Hello, world</p>
</div>
```


```js:実現するJavaScript
// ノードの作成
const container = document.createElement('div');
const p = document.createElement('p');
const pAttr = document.createAttribute('data-foo');
const comment = document.createComment('下記は段落要素');
const text = document.createTextNode('Hello, world');

// プロパティ、値の設定
container.id = 'container';
pAttr.value = 'bar';
p.setAttributeNode(pAttr);

// ノードの挿入
container.appendChild(comment);
p.appendChild(text);
container.appendChild(p);
document.body.appendChild(container);
```
- 実際はこのくらいのDOMツリーであればinnerHTMLを使った方が早くて簡単です。ただ、innerHTMLはHTMLを文字列として記述する必要があるので、その時々で使い分けるのが良いでしょう。(ノードの挿入については後述)

```js
const container = document.createElement('div');
container.id = 'container';
container.innerHTML = `
	<!-- 下記は段落要素 -->
	<p data-foo="bar">Hello, world</p>
`;

document.body.appendChild(container);
```

## ノードの挿入
作成したノードをDOMツリーに挿入するためのメソッドです。

### appendChild()
```js
親となる要素ノード.appendChild(挿入するノード);
```
- 親となる要素ノードの**最後（lastChildに相当）に挿入**します。

例）

```html
<ul id="lists">
	<li>トマト</li>
	<li>キュウリ</li>
	<li>レタス</li>
	<!-- ここに<li>ブロッコリー</li>を入れたい -->
</ul>
```
```js
// 挿入するノードの作成・設定
const lists = document.getElementById('lists');
const li = document.createElement('li');
li.appendChild(document.createTextNode('ブロッコリー'));

// ul#listsの最後の子ノードに追加
lists.appendChild(li);
```

### insertBefore()

```js
親となる要素ノード.insertBefore(挿入するノード, 子ノード);
```
- 第二引数に指定した**子ノードの前**に、第一引数のノードを挿入します。

例）

```html
<ul id="lists">
	<li>トマト</li>
	<li>キュウリ</li>
	<!-- ここに<li>ブロッコリー</li>を入れたい -->
	<li>レタス</li>
</ul>
```
```js
// 挿入するノードの作成・設定
// 略（appendChild()の例と同じ）

// ul#listsの最後の子要素ノードの前に挿入
lists.insertBefore(li, lists.lastElementChild);
```


### ノードの移動
上記appendChild()、insertBefore()メソッドを既存のノードに対して行うと、そのノードは**移動**します。
※厳密にいうと、現在の場所から削除され新たな場所に挿入されます。

```html
<ul id="lists">
	<li>トマト</li>
	<!-- ここに<li>ブロッコリー</li>を移動したい -->
	<li>キュウリ</li>
	<li>レタス</li>
	<li>ブロッコリー</li>
</ul>
```
```js
const lists = document.getElementById('lists');
const broccoli = lists.lastElementChild;
lists.insertBefore(broccoli, lists.children[1]);
``` 

### ノードのDOMへの挿入は毎回再描画されるのでcreateDocumentFragmentを利用する
ドキュメントのDOMツリーに変更がある度、ブラウザはリフロー（再描画）を行います。
多少の変更であれば問題はありませんが、要素を何百、何千など大量に追加するときなどはパフォーマンスに影響が出る可能性があるので注意が必要です。

悪い例）

```html
<ul id="lists"></ul>
```
```js
const lists = document.getElementById('lists');

// 1万個のliを都度DOMツリーに追加
for (let i = 0; i < 10000; i++) {
	const li = document.createElement('li');
	li.appendChild(document.createTextNode(i));
	lists.appendChild(li);
}
```

上記では1万回ドキュメントのDOMツリーに変更を加えていることになりよろしくありません。
解決方法はいくつかありますが、上記のコードあまり変えることなく改善するには**createDocumentFragment()メソッド**を使うのがスマートです。

```js
const fragment = document.createDocumentFragment();
```

createDocumentFragment()を実行すると**DocumentFragmentというノード**を作成します。
DocumentFragmentノードは、「ドキュメントの断片」「小型のdocumentのようなもの」と呼ばれることがあり、どういうことかというと**DocumentFragmentをルートとしたツリー構造を作成**できます。

```text:通常のDOMツリー
document
 └ html
    ├ head
    └ body
        ├ h1
        │  └ 見出しテキスト
        └ p
           └ テキストテキストテキスト
```
```text:DocumentFragmentのツリー
DocumentFragment
  ├ li
  │  └ 1
  ├ li
  │  └ 2
  ├ li
  │  └ 3
  ～略
```

このツリーはメモリ上にのみ存在し描画に利用されるDOMツリーには現れません。そのため**子ノードを追加してもリフローが発生しません。**
DOMツリーに追加されると、子ノードに置き換わります。

```text:上記の通常のDOMツリーにDocumentFragmentを挿入
document
 └ html
    ├ head
    └ body
        ├ h1
        │  └ 見出しテキスト
        ├ p
        │  └ テキストテキストテキスト
        ├ li
        │  └ 1
        ├ li
        │  └ 2
        ├ li
        │  └ 3
        ～略
```

先ほどの悪い例をcreateDocumentFragment()を使って書き直すと下記になります。

```js
const lists = document.getElementById('lists');

// 空のDocumentFragmentノードを作成
const fragment = document.createDocumentFragment();

for (let i = 0; i < 10000; i++) {
	const li = document.createElement('li');
	li.appendChild(document.createTextNode(i));
	// DocumentFragmentノードに追加するがメモリー上なのでリフローは行われない
	fragment.appendChild(li);
}

// 1回だけリフロー発生
lists.appendChild(fragment);
```
1万回リフロー発生するのが1回で収まり、パフォーマンスも改善されました。


## ノードの削除

```js
親となる要素ノード.removeChild(子ノード);
```
- 削除するのは**子ノード**となります。

例）

```html
<ul id="lists">
    <li>トマト</li>
    <li>キュウリ</li>
    <li>レタス</li>
    <li>ブロッコリー</li><!-- 削除したい -->
</ul>
```
```js
const lists = document.getElementById('lists');
const broccoli = lists.lastElementChild;
lists.removeChild(broccoli);
```

### 親要素が不明、不定の場合
```js
// 親要素が不明な野ノード
const target = document.getElementById('foo');

if (target.parentNode) {
 target.parentNode.removeChild(target);
}
```

### 要素のすべての子ノードを削除
```js
// 子ノードを削除する要素
const target = document.getElementById('foo');

while (target.firstChild) {
 target.removeChild(target.firstChild);
}
```

この方法には冒頭で挙げた、対象の要素のinnerHTMLプロパティ（textContentプロパティでも可）に空文字を代入する方法もありますが、その場合だと**削除したノードを再利用できません**。

またinnerHTMLよりもremoveChild()の方が[パフォーマンスが良いという記事](https://jsperf.com/innerhtml-vs-removechild/157)も見かけますが、それは[昔の話という記事](http://qiita.com/Hirakawa_Yuzo/items/1a2f8a22db41095b06a0#removechild)も…。
自分も試した所感ですが、基本的にはシンプルな`innerHTML = '';`で良いのではと感じます。

### 削除したノードは再利用できる
削除された子ノードは変数に保存することで、**再利用が可能**です。
こちらに関しては[デモを作成しました。]  (https://codepen.io/KDE_SPACE/pen/MvxyEe)
デモから抜粋すると下記の部分で、削除したノードを配列に格納して再利用しています。

```js
/**
 * 最後のリストを削除し、配列に格納
 */
const removeList = () => {
	const lastLi = lists.lastElementChild;
	listRemoved.push(lists.removeChild(lastLi));
}

/**
 * 配列の末尾から抽出しDOMに挿入。（配列から削除）
 */
const revivalList = () => {
	lists.appendChild(listRemoved.splice(-1,1)[0]);
}
```

## ノードの置換
```js
親となる要素ノード.replaceChild(新しいノード, 子ノード);
```
- 置換されるのは**子ノード**となります。
使い方としてはinsertBefore()メソッドと同じように、親ノードを起点に、第二引数に指定した子ノードを第一引数のノードに置き換えます。

例）

```html
<div id="target">
	<p id="current">Hello, world.</p>
	<!-- <p id="new">Goodbye</p> に置換したい-->
</div>
```
```js
const target = document.getElementById('target');
const currentP = document.getElementById('current');
const newP = document.createElement('p');
newP.id = 'new';
newP.textContent = 'Goodbye';

target.replaceChild(newP, currentP);
```

## その他新しいメソッド
insertBefore()やremoveChild()、replaceChild()は親要素を起点に考えなくてはならず直感的にわかりにくいところがあります。
しかし最近になって追加された下記メソッドではその問題を解消できます。（各々MDNのサイトにリンクしています）

- [before()](https://developer.mozilla.org/ja/docs/Web/API/ChildNode/before) 
- [after()](https://developer.mozilla.org/ja/docs/Web/API/ChildNode/after)
- [replaceWith()](https://developer.mozilla.org/ja/docs/Web/API/ChildNode/replaceWith)
- [remove()](https://developer.mozilla.org/ja/docs/Web/API/ChildNode/remove)
- [prepend()](https://developer.mozilla.org/ja/docs/Web/API/ParentNode/prepend)
- [append()](https://developer.mozilla.org/ja/docs/Web/API/ParentNode/append)

これらはjQueryにあった同名のメソッドから影響を受けて作成されたメソッドです。
基本的に**IEとEdgeには非対応**ですが、それぞれMDNのサイトでポリフィルが記載されています。


# 参考
- [徹底マスター 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)
- [element.innerHTML - Web API インターフェイス | MDN](https://developer.mozilla.org/ja/docs/Web/API/Element/innerHTML)
- [Node.textContent - Web API インターフェイス | MDN](https://developer.mozilla.org/ja/docs/Web/API/Node/textContent)
- [Node.appendChild - Web API インターフェイス | MDN](https://developer.mozilla.org/ja/docs/Web/API/Node/appendChild)
- [Node.removeChild - Web API インターフェイス | MDN](https://developer.mozilla.org/ja/docs/Web/API/Node/removeChild)
- [Node.replaceChild - Web API インターフェイス | MDN](https://developer.mozilla.org/ja/docs/Web/API/Node/replaceChild)
- [Document.createDocumentFragment() - Web API インターフェイス | MDN](https://developer.mozilla.org/ja/docs/Web/API/document/createDocumentFragment)
- [七章第四回　ノードをまとめて扱う: DocumentFragment — JavaScript初級者から中級者になろう — uhyohyo.net](http://uhyohyo.net/javascript/7_4.html)
- [innerHTML vs removeChild · jsPerf](https://jsperf.com/innerhtml-vs-removechild/67)
- [JavaScriptプログラミングで性能を気にした書き方をしてみる - Qiita](http://qiita.com/Hirakawa_Yuzo/items/1a2f8a22db41095b06a0#removechild)
- [これからのDOM API - 要素を追加・削除するAPI | CodeGrid](https://app.codegrid.net/entry/2017-newer-dom-3)