ノードの複製(cloneNode)

以下では、DOMの「ノードの複製(cloneNode)」を、仕組み・注意点・実用パターンまで体系的に解説します。

概要

Node.prototype.cloneNode(deep = false) は、あるノードをコピーして新しいノードを生成します。コピーはツリー外で作られるため、実際に画面へ反映するには appendChildreplaceWith などで既存DOMに挿入します。

  • 浅い複製(shallow): node.cloneNode() / cloneNode(false)
    ノード自身と属性だけを複製。子ノードは含まれません。

  • 深い複製(deep): node.cloneNode(true)
    子孫ノードを含む完全なサブツリーを複製。

html
<ul id="list"> <li class="item"><b>Apple</b></li> </ul> <script> const li = document.querySelector('#list .item'); const shallow = li.cloneNode(); // <li class="item"></li> ※中の<b>…</b>は無い const deep = li.cloneNode(true); // <li class="item"><b>Apple</b></li> document.querySelector('#list').appendChild(deep); </script>

複製時に「コピーされるもの / されないもの」

  • コピーされる

    • 属性id, class, data-*, style など)

    • 子孫ノードdeep=true の場合)

    • インラインイベント属性(例: onclick="...")※属性なのでコピー対象

  • コピーされない

    • イベントリスナaddEventListener で登録したものは複製されません)

    • JS で動的に付与したプロパティ(例: element.someCustomProp = ...

    • 計算済みスタイル(Computed Style)。style 属性は複製されるが、CSSにより結果的に適用された見た目は「再計算」されます。

    • 一部の内部状態(後述のフォーム/メディア/キャンバス参照)

実用上は「イベントは引き継がれない」と覚え、再バインドまたはイベント委譲を使うのが定石です。

フォーム要素の挙動(重要)

フォーム要素は 属性値現在値(プロパティ) が分かれるため注意します。

  • input[type="text"], textarea現在の入力値 は、実装差の歴史があるため、自分で同期するのが安全です。

  • input[type="checkbox"|"radio"]チェック状態 も同様に、属性(checked)プロパティ(checked) を意識して同期してください。

  • select選択状態selected)も明示同期を推奨。

  • input[type="file"]セキュリティ上、ファイル選択状態は複製されません(常に空)。

安全な同期パターン例:

js
function cloneWithFormState(node) { const clone = node.cloneNode(true); const srcControls = node.querySelectorAll('input, textarea, select'); const dstControls = clone.querySelectorAll('input, textarea, select'); srcControls.forEach((src, i) => { const dst = dstControls[i]; switch (src.type) { case 'checkbox': case 'radio': dst.checked = src.checked; break; case 'select-one': case 'select-multiple': Array.from(dst.options).forEach((opt, idx) => { opt.selected = src.options[idx].selected; }); break; case 'file': // 何もしない(選択状態はコピー不可) break; default: dst.value = src.value; } }); return clone; }

メディア要素・キャンバス・スクリプトの挙動

  • <video> / <audio> の再生位置や再生状態は引き継がれません(属性初期状態に戻る)。

  • <canvas>描画内容(ビットマップ)は複製されません。必要なら drawImage で転写します。

    js
    function cloneCanvasWithPixels(src) { const dst = src.cloneNode(); // サイズや属性はコピー dst.width = src.width; // 念のためサイズを明示 dst.height = src.height; dst.getContext('2d').drawImage(src, 0, 0); return dst; }
  • <script> をクローンして挿入しても実行されません。必要なら改めて document.createElement('script') で生成し直し、srctextContent を設定します。

id の重複とアクセシビリティ属性

deep=true で複製すると、子孫にある id が丸ごと重複 します。label[for], aria-labelledby などの 参照関係 が壊れる原因になるため、複製後に id を付け替えるのが安全です。

js
function renewIds(root, prefix = 'cloned-') { root.querySelectorAll('[id]').forEach((el, idx) => { const old = el.id; const nu = prefix + old + '-' + idx; // 参照属性も同時に更新したければここで置換(for, aria-labelledby など) el.id = nu; }); }

イベントの再バインドとイベント委譲

  • 再バインド: クローン後に必要なイベントを付け直す

    js
    function wireCardEvents(card) { card.querySelector('.remove')?.addEventListener('click', () => card.remove()); card.querySelector('.like')?.addEventListener('click', () => card.classList.toggle('liked')); } const cloned = originalCard.cloneNode(true); wireCardEvents(cloned); list.appendChild(cloned);
  • イベント委譲: 親でまとめてイベントを受ける(クローンでも不要な再バインドが減る)

    js
    list.addEventListener('click', (e) => { const card = e.target.closest('.card'); if (!card) return; if (e.target.matches('.remove')) card.remove(); if (e.target.matches('.like')) card.classList.toggle('liked'); });

テンプレートとの併用(高速・安全)

頻繁に複製する UI は <template> を使うと高速・安全です。<template> の中身は非表示かつ非アクティブで、content.cloneNode(true) で必要なときに深い複製を得られます。

html
<template id="item-tpl"> <li class="item"> <span class="name"></span> <button class="remove" type="button">Remove</button> </li> </template> <ul id="items"></ul> <script> const tpl = document.getElementById('item-tpl'); function addItem(name) { const frag = tpl.content.cloneNode(true); // DocumentFragment const li = frag.querySelector('.item'); li.querySelector('.name').textContent = name; // 委譲を使えばここでイベント付与は不要 document.getElementById('items').appendChild(frag); } </script>

文書・フレーム間の複製

  • 近年のブラウザでは、別ドキュメントから持ってきたノードを appendChild するときに自動的に**養子化(adopt)**されるため、importNode を明示的に呼ばなくても動くことが多いです。互換性を厳密に見るなら document.importNode(node, true) を使ってから挿入します。

よくある落とし穴チェックリスト

  1. deep を付け忘れて子要素が消える

  2. 複製後の id 重複 を放置

  3. イベントが引き継がれない(再バインド or 委譲の採用)

  4. フォーム状態がズレる(値/チェック/選択を明示同期)

  5. <canvas> の 描画内容が消える(ビットマップは別途コピー)

  6. <script> が動かない(新規生成で挿入)

  7. 大きなサブツリーを頻繁に複製して パフォーマンス低下(<template> + DocumentFragment を活用)

  8. structuredClone は DOM を複製しない(オブジェクト用API。DOMには cloneNode を使う)

まとめ

  • cloneNode(false) は「骨格(要素本体+属性)」、cloneNode(true) は「サブツリー丸ごと」を複製。

  • イベントは引き継がれないため、再バインドまたはイベント委譲を活用。

  • フォーム・メディア・キャンバスなどは状態がそのままにはならないことがあるため、明示的に同期/転写する。

  • 繰り返し使うUIは <template> + content.cloneNode(true) が最適解。

ChatGPT5 生成日:2025/09/11