The browser gives you node.cloneNode(true) to deep-copy a DOM subtree — but implementing it yourself is a great exercise in recursion over a heterogeneous tree. A DOM node isn't just elements: it's elements (with attributes), text nodes, and comments, and a clone has to reproduce each kind faithfully so the copy is a fully independent tree. Change the clone, and the original mustn't budge.
Implement deepCloneDom(node) without cloneNode. Recreate the node by type — element (tag + attributes + cloned children), text, or comment — recursing into children so the whole subtree is duplicated.
function deepCloneDom(node) {
// returns a new, independent copy of node and its subtree
}
const clone = deepCloneDom(el('<p>before<b>bold</b>after</p>'));
clone.outerHTML; // '<p>before<b>bold</b>after</p>'
clone !== original; // true
clone.querySelector('b') !== original.querySelector('b'); // true (deep)
nodeType — element = 1, text = 3, comment = 8. Each gets its own factory: createElement, createTextNode, createComment.element.attributes and setAttribute(name, value) on the clone.childNodes — not children, so text and comment nodes come along too; append each cloned child.You'll recurse over the subtree, dispatching on nodeType: rebuild elements (tag + attributes + cloned children), text nodes, and comments with the matching document.create* factory.
A deep clone is a post-order rebuild: to clone an element you must first clone its children, then attach them. The DOM is a heterogeneous tree, so the recursion has to handle three node kinds. An element carries a tag name and attributes and has children — recreate it with createElement, copy each attribute, and recurse into every child. A text node is just a string — createTextNode. A comment likewise — createComment. Because you build every node from scratch (never sharing a reference with the original), the result is a completely independent tree.
Ask each node "what kind are you?" and build the matching new node:
document.createTextNode(node.textContent).document.createComment(node.textContent).document.createElement(node.tagName), then copy every attribute, then for each child call yourself and appendChild the result.The element case is the only one that recurses, and it recurses over childNodes (not children) so text and comment children ride along. The recursion bottoms out at leaves (text nodes, or empty elements).
The tempting shortcut copies innerHTML, which loses fidelity and isn't really a node clone:
function deepCloneDomNaive(node) {
const clone = document.createElement(node.tagName);
clone.innerHTML = node.innerHTML; // re-parses HTML — brittle
return clone;
}
Several problems. It ignores the node's own attributes (only its inner markup is copied). It assumes node is always an element — a text or comment node has no tagName/innerHTML and would throw. And round-tripping through innerHTML re-parses a string, which can normalize or mangle content (and drops anything not serializable back to identical HTML). A structural, node-by-node clone that branches on nodeType avoids all of it.
function deepCloneDom(node) {
// Text node.
if (node.nodeType === 3) {
return document.createTextNode(node.textContent);
}
// Comment node.
if (node.nodeType === 8) {
return document.createComment(node.textContent);
}
// Element node: tag + attributes + cloned children.
if (node.nodeType === 1) {
const clone = document.createElement(node.tagName);
for (const attr of node.attributes) {
clone.setAttribute(attr.name, attr.value);
}
for (const child of node.childNodes) {
clone.appendChild(deepCloneDom(child)); // recurse
}
return clone;
}
return null; // other node types (e.g. document fragments) — not handled here
}
module.exports = { deepCloneDom };
Each branch matches a nodeType. Text and comment nodes are leaves — one create* call. The element branch recreates the tag, then copies attributes by iterating node.attributes (a live NamedNodeMap of { name, value }), then recurses over node.childNodes — appending each cloned child. Using childNodes (not children) means text and comment children are cloned too, preserving mixed content like before<b>bold</b>after. Every returned node is freshly created, so the clone shares no references with the source: it's O(n) over the nodes, and fully independent.
deepCloneDom on <p>before<b>bold</b>after</p>:
p (element) — createElement('P'). No attributes. Recurse over its three child nodes.
"before" → createTextNode('before'), appended.b (element) → createElement('B'), recurse into its child.
"bold" → createTextNode('bold'), appended to the new b.b, appended to the new p."after" → createTextNode('after'), appended.<p>before<b>bold</b>after</p>. Editing the clone's <b> leaves the original untouched.tagName; branch on nodeType first (text 3, comment 8, element 1).children vs childNodes — children skips text/comment nodes, so mixed content is lost. Recurse over childNodes.innerHTML) drops class, href, data-*. Iterate node.attributes.innerHTML round-trip — re-parsing a string isn't a faithful node clone and can normalize content; build nodes structurally.value on inputs, checked state, and event listeners aren't attributes and won't copy; a "clone with state" needs to read the live properties (this is why forms often lose their values on a naive clone).importNode across documents — copying a node into a different document (e.g. from a <template>) uses document.importNode, which is the cross-document cousin of this recursion.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.
The browser gives you node.cloneNode(true) to deep-copy a DOM subtree — but implementing it yourself is a great exercise in recursion over a heterogeneous tree. A DOM node isn't just elements: it's elements (with attributes), text nodes, and comments, and a clone has to reproduce each kind faithfully so the copy is a fully independent tree. Change the clone, and the original mustn't budge.
Implement deepCloneDom(node) without cloneNode. Recreate the node by type — element (tag + attributes + cloned children), text, or comment — recursing into children so the whole subtree is duplicated.
function deepCloneDom(node) {
// returns a new, independent copy of node and its subtree
}
const clone = deepCloneDom(el('<p>before<b>bold</b>after</p>'));
clone.outerHTML; // '<p>before<b>bold</b>after</p>'
clone !== original; // true
clone.querySelector('b') !== original.querySelector('b'); // true (deep)
nodeType — element = 1, text = 3, comment = 8. Each gets its own factory: createElement, createTextNode, createComment.element.attributes and setAttribute(name, value) on the clone.childNodes — not children, so text and comment nodes come along too; append each cloned child.You'll recurse over the subtree, dispatching on nodeType: rebuild elements (tag + attributes + cloned children), text nodes, and comments with the matching document.create* factory.
A deep clone is a post-order rebuild: to clone an element you must first clone its children, then attach them. The DOM is a heterogeneous tree, so the recursion has to handle three node kinds. An element carries a tag name and attributes and has children — recreate it with createElement, copy each attribute, and recurse into every child. A text node is just a string — createTextNode. A comment likewise — createComment. Because you build every node from scratch (never sharing a reference with the original), the result is a completely independent tree.
Ask each node "what kind are you?" and build the matching new node:
document.createTextNode(node.textContent).document.createComment(node.textContent).document.createElement(node.tagName), then copy every attribute, then for each child call yourself and appendChild the result.The element case is the only one that recurses, and it recurses over childNodes (not children) so text and comment children ride along. The recursion bottoms out at leaves (text nodes, or empty elements).
The tempting shortcut copies innerHTML, which loses fidelity and isn't really a node clone:
function deepCloneDomNaive(node) {
const clone = document.createElement(node.tagName);
clone.innerHTML = node.innerHTML; // re-parses HTML — brittle
return clone;
}
Several problems. It ignores the node's own attributes (only its inner markup is copied). It assumes node is always an element — a text or comment node has no tagName/innerHTML and would throw. And round-tripping through innerHTML re-parses a string, which can normalize or mangle content (and drops anything not serializable back to identical HTML). A structural, node-by-node clone that branches on nodeType avoids all of it.
function deepCloneDom(node) {
// Text node.
if (node.nodeType === 3) {
return document.createTextNode(node.textContent);
}
// Comment node.
if (node.nodeType === 8) {
return document.createComment(node.textContent);
}
// Element node: tag + attributes + cloned children.
if (node.nodeType === 1) {
const clone = document.createElement(node.tagName);
for (const attr of node.attributes) {
clone.setAttribute(attr.name, attr.value);
}
for (const child of node.childNodes) {
clone.appendChild(deepCloneDom(child)); // recurse
}
return clone;
}
return null; // other node types (e.g. document fragments) — not handled here
}
module.exports = { deepCloneDom };
Each branch matches a nodeType. Text and comment nodes are leaves — one create* call. The element branch recreates the tag, then copies attributes by iterating node.attributes (a live NamedNodeMap of { name, value }), then recurses over node.childNodes — appending each cloned child. Using childNodes (not children) means text and comment children are cloned too, preserving mixed content like before<b>bold</b>after. Every returned node is freshly created, so the clone shares no references with the source: it's O(n) over the nodes, and fully independent.
deepCloneDom on <p>before<b>bold</b>after</p>:
p (element) — createElement('P'). No attributes. Recurse over its three child nodes.
"before" → createTextNode('before'), appended.b (element) → createElement('B'), recurse into its child.
"bold" → createTextNode('bold'), appended to the new b.b, appended to the new p."after" → createTextNode('after'), appended.<p>before<b>bold</b>after</p>. Editing the clone's <b> leaves the original untouched.tagName; branch on nodeType first (text 3, comment 8, element 1).children vs childNodes — children skips text/comment nodes, so mixed content is lost. Recurse over childNodes.innerHTML) drops class, href, data-*. Iterate node.attributes.innerHTML round-trip — re-parsing a string isn't a faithful node clone and can normalize content; build nodes structurally.value on inputs, checked state, and event listeners aren't attributes and won't copy; a "clone with state" needs to read the live properties (this is why forms often lose their values on a naive clone).importNode across documents — copying a node into a different document (e.g. from a <template>) uses document.importNode, which is the cross-document cousin of this recursion.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.