skip to content

In the browser DOM, what is the difference between node.cloneNode(true) and node.cloneNode(false), and what state does cloning fail to carry over to the copy?

level: middleimportance: should knowfreq 54%

answer

  1. the argument is the deep flag
  2. attributes yes, JavaScript object no
  3. listeners do not survive the copy
  4. the copy has no parent yet
  5. ids get duplicated across copies

basics

~20 s

cloneNode(true) copies the node and its whole subtree; cloneNode(false) copies only the node itself with its attributes. Either way the copy is detached and carries no listeners added with addEventListener and no custom JavaScript properties.

solid answer

~50 s

The boolean is the *deep* flag. `cloneNode(true)` copies the node plus every descendant; `cloneNode(false)` — which is also the default when you pass nothing — copies just that node, with its attributes but no children. In both cases the result is a new, detached node with no parent, so you must insert it yourself. The important part is what does not come across: listeners registered with `addEventListener` are not cloned, and neither are expando properties you set on the element, because cloning copies the node's specified attributes and content, not the JavaScript object graph around it. Attributes *do* come across, which means an inline `onclick="…"` handler survives while an `addEventListener` one does not, and it also means any `id` is duplicated — cloning a subtree with ids and inserting it gives you duplicate ids in the document. A cloned `<script>` that has already run will not run again when inserted.

code

javascript · 16 lines
javascript
const li = document.createElement('li');
li.id = 'row';
li.dataset.key = 'k1';
li.myOwnProp = 'expando';
li.setAttribute('onclick', "console.log('inline fires')");
li.addEventListener('click', () => console.log('listener fires'));
li.append(document.createElement('span'));

const shallow = li.cloneNode(false);
const deep = li.cloneNode(true);

console.log(shallow.children.length, deep.children.length); // 0 1
console.log(deep.dataset.key);        // "k1"  - attribute copied
console.log(deep.myOwnProp);          // undefined - expando not copied
console.log(deep.parentNode);         // null - you must insert it
console.log(deep.id === li.id);       // true - duplicate id waiting to happen

go deeper

for a junior

Know that the boolean means deep, that cloneNode(true) copies the children and cloneNode(false) does not, and that the copy is not on the page until you insert it yourself.

for a middle

Explain the copy boundary: attributes and subtree yes, the JavaScript object's listeners and custom properties no. Be ready to contrast inline onclick surviving with addEventListener listeners not surviving.

for a senior

Show the production consequences — duplicate ids breaking label/for and getElementById, behaviour needing to be re-attached or delegated — and say when a move is the better tool than a clone.

for a principal

Own the pattern choice: hand-cloned prototypes carry hidden coupling between markup and wiring code, so decide where that is acceptable versus where a component model with an explicit render contract should own node creation.

## The deep flag `cloneNode(deep)` takes one boolean. In the current DOM standard the parameter is optional and defaults to `false`, so `el.cloneNode()` is a shallow clone. Older DOM levels made the argument mandatory, which is why a lot of code writes the value explicitly — a habit worth keeping for readability. - **Shallow (`false`)** — you get the node itself: same node type, same tag name, same attributes, and nothing inside it. Cloning a populated `<ul>` shallowly gives you an empty `<ul>` that still has its `class` and `id`. - **Deep (`true`)** — you get the node and a recursive copy of every descendant: elements, text nodes, comments, the lot. Either way, the clone is **detached**: its `parentNode` is `null` and it appears nowhere until you insert it. The clone belongs to the same `ownerDocument` as the original. ```js const row = document.querySelector('#template-row'); const shallow = row.cloneNode(); // <tr> with attributes, no cells const deep = row.cloneNode(true); // <tr> with all its cells table.append(deep); ``` ## What is copied Cloning works from the node's markup-level state: - The tag name and namespace. - Every **attribute** and its value — `class`, `id`, `data-*`, `style`, `disabled`, and inline event-handler content attributes such as `onclick`. - For a deep clone, the entire descendant tree, including text and comment nodes. ## What is not copied This is the interview payload: 1. **Event listeners added with `addEventListener`.** Listeners live on the `EventTarget` object; cloning creates a *different* object, so the copy starts with none. This is the single most-asked fact about `cloneNode`, and it is the sharpest contrast with *moving* a node, which preserves listeners exactly because it is the same object. 2. **Expando properties.** `el.myData = record` is a property of the JavaScript object, invisible to cloning. If you want per-node data to survive cloning, put it in a `data-*` attribute, which is markup. 3. **The parent.** The clone is not inserted anywhere; forgetting that step is the classic "my clone never appeared" bug. And one behavioural carry-over that surprises people in the other direction: a `<script>` element that has already executed carries its already-started state to the clone, so inserting the clone will not run the script a second time. ## The duplicate-id trap Because `id` is an attribute, it is copied. Deep-cloning a card that contains `<label for="email">` and `<input id="email">` and inserting it three times produces three elements with `id="email"`. `document.getElementById` returns only the first, and every label now points at the same field, which breaks the form for keyboard and screen-reader users. Anything cloned repeatedly must either avoid ids in favour of classes and relative queries, or rewrite them on each copy: ```js const copy = card.cloneNode(true); copy.querySelectorAll('[id]').forEach(el => { el.id = `${el.id}-${n}`; }); ``` ## Clone versus move The two operations are complements and choosing between them is the real skill: | | Move (re-insert) | Clone and insert | |---|---|---| | Original still exists | no | yes | | Listeners preserved | yes | no | | Expando properties preserved | yes | no | | Duplicate ids | impossible | likely | Rendering N rows from one prototype is a clone job — and the corollary is that you attach behaviour *after* cloning, or better, use one delegated listener on the container so per-row listeners never need to exist. ## Cross-document copies `cloneNode` always produces a node owned by the original's document. To copy a node out of another document — say an element from a same-origin iframe's `contentDocument` — use `document.importNode(node, true)`, which creates a copy owned by the calling document and leaves the original in place. (Simply *inserting* a foreign node adopts it into your document instead, which moves it rather than copying it.) ## Practical rules Default to `cloneNode(true)` when you mean "copy this thing", pass `false` deliberately when you want an empty shell of the same element, insert the result explicitly, strip or rewrite ids on anything cloned more than once, and never expect behaviour to come along for the ride.

  • Why does an inline onclick attribute survive cloneNode while an addEventListener listener does not?
    Because they live in different places. `onclick="…"` is a content attribute, part of the node's markup state, and cloning copies attributes. A listener registered with `addEventListener` is stored in the `EventTarget`'s internal listener list, which belongs to that specific object; the clone is a different object and starts with an empty list.
  • What is a safer way to give 200 cloned rows click behaviour than re-attaching a listener to each clone?
    Attach one listener to the container instead of one per row, and identify the row from the event inside the handler. That keeps the cost independent of row count and means clones need no wiring at all. If a row must carry data, put it in a `data-*` attribute so it survives cloning.
  • When would you reach for document.importNode instead of cloneNode?
    When the source node belongs to a different document — for example an element read out of a same-origin iframe's `contentDocument`. `importNode(node, true)` returns a copy owned by the calling document and leaves the original where it is. `cloneNode` would produce a node still owned by the foreign document.

saying these in an interview costs you the question

  • Expecting addEventListener listeners to be cloned
  • Thinking cloneNode inserts the copy into the document
  • Assuming cloneNode() with no argument does a deep copy
  • Cloning subtrees with ids and ignoring the duplicates
  • Believing expando properties on the element are copied

context