jQuery.prototype.html の仕様を調べてみた

# 概要

jQuery の `.html` で公式にない仕様を確認した為、jQuery v3.1.1 を元に仕様を調査してみました。

# jQuery.prototype

この節は本題とは無関係なので読み飛ばしても構いません。

いわゆる「jQuery オブジェクト」と呼ばれるものは `new jQuery` のようにして生成されており、`jQuery.prototype` 上にあるメソッドを扱えます。

```JavaScript
console.log(jQuery('p') instanceof jQuery);               // true
console.log(new jQuery('p') instanceof jQuery);           // true
console.log(jQuery('p').html === jQuery.prototype.html);  // true
console.log(jQuery.fn === jQuery.prototype);              // true
```

jQuery plugin を作る時に `jQuery.fn` にメソッドを代入していますが、内部的には `jQuery.prototype` に代入されています。
また、`jQuery('p')` と `new jQuery('p')` は等価です。

本記事では `jQuery.prototype.empty` を `jQuery#empty` と表記します。
jQueryの公式ドキュメント上では `jQuery.prototype.html` を `.html()` のように表記しています。

- [.html() | jQuery API Documentation](http://api.jquery.com/html/)

# .html()

引数なしの `.html()` は DOM API である `element.innerHTML` の getter として評価されます。

```HTML:jQuery#htmlでgetter処理
<div id="sample">This is a sample.</div>

<script>
console.log(jQuery('#sample').html());  // "This is a sample."
</script>
```

他の多くのgetter系 jQuery API と同じく、`.html()` は jQuery オブジェクト群の**初めの要素ノードに対して処理**されます。

```HTML:jQuery#htmlで先頭の要素に対してgetter処理
<div class="sample">This is a pen.</div>
<div class="sample">This is a book.</div>
<div class="sample">This is a eraser.</div>

<script>
console.log(jQuery('.sample').html());  // "This is a pen.<"
</script>
```

# .html( htmlString )

`.html()` に `String` 型の引数が与えられた場合、DOM API である `element.innerHTML` の setter として評価されます。

```HTML:jQuery#htmlでsetter処理
<div id="sample">This is a sample.</div>

<script>
console.log(jQuery('#sample').html('<strong>This is a jQuery</strong>'));  // [div#sample] ← 評価値は jQuery('#sample') になります
```

`.html(htmlString)` は対応する jQuery オブジェクト群の**全てに適用**されます。

```HTML:jQuery#htmlで先頭の要素に対してgetter処理
<!-- 下記3つとも "<strong>This is a jQuery</strong>" に置換される -->
<div class="sample">This is a pen.</div>
<div class="sample">This is a book.</div>
<div class="sample">This is a eraser.</div>

<script>
console.log(jQuery('.sample').html('<strong>This is a jQuery</strong>'));  // [div.sample, div.sample, div.sample]
</script>
```

# .html( function )

関数を引数にした `.html()` はコールバック関数に「インデックス」「`element.innerHTML`」を渡して全てのjQueryオブジェクトに `innerHTML` の setter 処理を行います。

```HTML:jQuery#htmlでコールバック処理
<!-- 下記3つとも '<strong>" + htmlString + '"</strong>'' に置換される -->
<div class="sample">This is a pen.</div>
<div class="sample">This is a book.</div>
<div class="sample">This is a eraser.</div>

<script>
console.log(jQuery('.sample').html(function (index, htmlString) {
  return '<strong>"' + htmlString + '"</strong>';
}));  // [div.sample, div.sample, div.sample]
```

# [隠し仕様] .html( DOMNode or jQueryObject) (v.3.1.1 現在)

公式リファレンスでは言及されていませんが、jQuery v3.1.1 ではDOMノードやjQueryオブジェクトを引数にとることもできるようです。
**(注意: この仕様はリファレンスにない以上、公式に認められた仕様ではないので今後の更新で削除される可能性があります。原則として使わない事をお勧めします。)**

jQueryオブジェクトが引数にとられた場合、当該要素の子要素を全て削除(`jQuery#empty` の処理が実行されます)した後に当該jQueryオブジェクトを `jQuery#append` します。

```HTML:jQuery#htmlで子要素群をjQueryオブジェクトで上書きする
<div id="sample">This is a sample.</div>

<script>
jQuery('#sample').html(jQuery('<strong>replaced text.</strong>'));
</script>
```

引数がDOMノードであった場合、一度 jQuery オブジェクトに変換してから先と同様の処理を行います。

```HTML:jQuery#htmlで子要素群をDOMノードで上書きする
<div id="sample-1">This is a pen.</div>
<div id="sample-2">This is a book.</div>
<script>
/**
 * 要素ノード
 */
var strong = document.createElement('strong');
strong.textContent = 'replaced text 1.';
jQuery('#sample-1').html(strong);

/**
 * DocumentFragment
 */
var df = document.createDocumentFragment(),
    p = document.createElement('p');

for (var i = 1; i < 4; ++i) {
  p.textContent = 'This is a sample-2-' + i;
  df.appendChild(p.cloneNode(true));
}

jQuery('#sample-2').html(df);
</script>
```

# まとめ

公式ドキュメントでは `.html()` が文字列を対象に取るメソッドと説明されている為、それに則るのが好ましいと思います。
DOMノードが必要なら次の2つの対策が考えられます。

- `jQuery(selector).empty().append(domNode)`
- DOM API で完結したコードを書く (`Node#removeChild` -> `appendChild`)

```HTML:子コード群を削除してDOMノードを上書き挿入する
<div id="parent-1">
  <div id="child-1-1">child-1-1</div>
  <div id="child-1-2">child-1-2</div>
  <div id="child-1-3">child-1-3</div>
</div>

<div id="parent-2">
  <div id="child-2-1">child-2-1</div>
  <div id="child-2-2">child-2-2</div>
  <div id="child-2-3">child-2-3</div>
</div>

<script>
'use strict';
/**
 * createDocumentFragment
 */
function createDFNode (textList) {
  var df = document.createDocumentFragment(),
      p = document.createElement('p');

  for (var i = 0, len = textList.length; i < len; ++i) {
    p.textContent = textList[i];
    df.appendChild(p.cloneNode(true));
  }

  return df;
}

/**
 * jQuery#empty -> jQuery#append (jQuery API)
 */
jQuery('#parent-1').empty().append(createDFNode(['text1-1', 'text1-2', , 'text1-3']));

/**
 * Node#removeChild -> appendChild
 */
var parent = document.getElementById('parent-2');
Array.from(parent.childNodes).forEach(function (node) {
  node.parentNode.removeChild(node);
});
parent.appendChild(createDFNode(['text2-1', 'text2-2', , 'text2-3']));
</script>
```

# 参考リンク

「公式リファレンス」と「jquery-3.1.1.js のソースコード(`.html` の該当行: 5978行目)」。

- [.html() | jQuery API Documentation](http://api.jquery.com/html/)
- [jquery.js v3.1.1 · jquery/jquery-ui](https://github.com/jquery/jquery-ui/blob/master/external/jquery-3.1.1/jquery.js#L5978)