Deep clone and deep equal
Copy or compare nested objects and arrays by walking into every level, remembering objects already seen so loops don't run forever.
O(n)Space O(n)4 practice problemsStep-through pictureThe idea
In JavaScript, a === b on two objects only checks whether they are the same object in memory. Two separate objects with the same contents are not ===. And { ...settings } copies only the top level: nested objects inside are still shared, so changing the copy's nested values changes the original too.
A deep clone makes a new object for every object and array inside, all the way down. A deep equal check compares two values the same way: same kind (array or object), same keys, and deep-equal values under each key. Both are recursive: they call themselves on each child value.
Objects can point back at themselves, like a tree node with a parent link. A naive recursion then goes around the loop forever. The fix is a seen map from each original object to its copy. Before copying an object, check seen: if it is there, reuse the copy. That also keeps shared references shared in the copy.
An everyday example
Copying a set of linked notebooks
You must copy a set of notebooks where pages say "see notebook B". You copy notebook A, and when a page points to B, you copy B too. But B has a page saying "see notebook A". If you just follow pointers, you copy A again, then B again, forever. So you keep a list: "original A is copy A2, original B is copy B2". When a pointer leads to a notebook already on the list, you point to its copy instead of copying again.
| In the example | In the code |
|---|---|
| A notebook | An object or array, value |
| The list of originals and their copies | seen, a Map |
| Already on the list | seen.has(value), so return seen.get(value) |
| Writing the list entry before copying the pages | seen.set(value, copy) before the loop |
| Copying each page | copy[key] = deepClone(value[key], seen) |
From the slow way to deep clone and deep equal
- Start with the obvious copies.
{ ...settings }andObject.assigncopy only the top level, socopy.themeis the very same object assettings.theme, and changing one changes the other.JSON.parse(JSON.stringify(settings))copies every level, but it dropsundefinedvalues and functions, turns aDateinto a string, and throws on data that points back at itself. - A real copy makes a new object for every object inside, all the way down. That is a recursion: a primitive like a number or string is returned as is, since it can't be changed anyway, and an object or array becomes a new empty one whose keys are filled by copying each child. For data shaped like a tree, that's enough.
- But app data isn't always a tree. A document can list the same user as
ownerand aseditors[0]. The simple recursion copies that user twice, so in the copy the two are different objects, and the shape has changed. Worse, if the user points back at the document, the recursion goes around the loop until the call stack overflows. - Both problems have one fix: remember what you've already copied. Keep
seen, aMapfrom each original object to its copy. Check it before copying, and record the new copy before filling it, so a loop back finds the half-built copy. That recursion with a memory is deep clone; deep equal is the same walk over two values at once, comparing kinds, key counts, and each key's values.
How it works
- If
valueisn't an object (a number, string, boolean,null, orundefined), return it as is. Primitives can't be changed, so sharing them is safe. - If
seenalready hasvalue, return its copy. This handles loops and shared objects. - Make an empty
copy, an array ifvalueis an array, otherwise a plain object. - Record
seen.set(value, copy)before copying children, so a child pointing back finds the copy. - For each key of
value, setcopy[key] = deepClone(value[key], seen), then returncopy. - For
deepEqual(a, b): returntrueifa === b;falseif either isn't an object or one is an array and the other isn't; otherwise compare key counts and then each key's values withdeepEqual.
Watch it run
This picture runs the code below on a real input. Press Step to move one step at a time, or Play to watch. The highlighted line is the one that just ran. Change the input to see other cases.
See every step at onceThe same run, laid out side by side
The code
Point at a note to light up the lines it explains.
function deepClone(value, seen = new Map()) {if (value === null || typeof value !== "object") return value;if (seen.has(value)) return seen.get(value);const copy = Array.isArray(value) ? [] : {};seen.set(value, copy);for (const key of Object.keys(value)) {copy[key] = deepClone(value[key], seen);}return copy;}function deepEqual(a, b) {if (a === b) return true;if (a === null || b === null || typeof a !== "object" || typeof b !== "object") return false;if (Array.isArray(a) !== Array.isArray(b)) return false;const keysA = Object.keys(a);const keysB = Object.keys(b);if (keysA.length !== keysB.length) return false;return keysA.every((key) => Object.hasOwn(b, key) && deepEqual(a[key], b[key]));}
How fast it is
| Time | O(n) |
|---|---|
| Extra memory | O(n) |
Each object, array, and key is handled once. The seen map and the recursion depth can grow with the number of objects.
- Best case
O(1)valueis a primitive, like42, so the first line returns it. FordeepEqual, the two values are the same reference or have different key counts at the top. - Average case
O(n)A typical nested state object: every object, array, and key is handled once.
- Worst case
O(n)Every value is an object with keys to visit. Shared objects and loops don't add work, because
seenreturns them at once.
Common mistakes
- Using
{ ...obj }orObject.assignand calling it a deep copy. Nested objects are still shared. - Using
JSON.parse(JSON.stringify(obj)). It dropsundefinedand functions, turnsDateinto strings, and throws on cycles. - Adding to
seenafter copying the children. Cycles then recurse forever. - Comparing only the keys of
aindeepEqual.{ a: 1 }would equal{ a: 1, b: 2 }. Compare key counts too.
When to use it
Look for these clues in a problem:
- The question asks for a copy that shares nothing with the original, or a check that two nested values have the same contents.
- The data is nested objects and arrays of any depth.
- It mentions cycles, shared references, or
structuredClone. - Changes to a copy must not affect the original, as with immutable state updates.
Check yourself
Answer each one in your head first, then open it.
What happens if seen.set(value, copy) is moved after the loop over the keys?
A loop back to value isn't found in seen while its children are being copied, so a new copy starts, which loops again, until the call stack overflows.
In the trace, why is copy.owner === copy.editors[0] true?
Both point to ida in the original. The first visit made ida's copy and stored it in seen, and the second visit got that same copy back.
What does deepEqual([1, 2], { 0: 1, 1: 2 }) return, and why?
false. The keys and values match, but Array.isArray is true for one and false for the other, and the code treats that as a difference.
What does deepEqual({ a: undefined }, { b: undefined }) return?
false. Both have one key, but Object.hasOwn(b, "a") is false. Without that check, b.a would read as undefined and they would wrongly look equal.
Practice problems
Stuck on one? Tuto is in the playground too, and gives you hints, never the answer.
0 of 4 solved. Solve one to earn the bronze medal, half for silver, and all of them for gold.
Start practicingGo further
You don't need these to start practicing. Open one when you want more.
Why it works
The reasoning that makes it always right
The key rule: seen maps every original object whose copy has been started to that one copy. The check seen.has(value) comes before making a copy, and seen.set(value, copy) comes right after, before any child is visited. So no object can be copied twice, even when a loop reaches it again while it's still being filled.
The copy has the same shape as the original. Each copy[key] is the clone of value[key]: primitives are equal, and objects map to their single copy. So if two places in the original point to the same object, the two places in the copy point to the same copy, and a loop in the original becomes a loop in the copy. Every object in the copy is new, so changing the copy never touches the original.
It ends because each object takes the "make a copy" path at most once, and each of its keys is visited once. Every other call returns at once, so the number of calls is at most the number of keys plus one. deepEqual is correct for tree-shaped data by the same reasoning, level by level: same kind, same number of keys, every key of a present in b with an equal value means both have exactly the same keys and values. It has no seen, so a loop makes it recurse forever.
Going deeper
3 more ideas
Why seen must be filled before the children
In node.parent.child === node, cloning node reaches parent, which reaches child, which is node again. If node were only added to seen after its children were copied, that inner visit wouldn't find it and would start another copy, forever. Adding it first means the inner visit finds the half-built copy and links to it, which is exactly right.
How fast it is
Every object and array is copied once and every key is visited once, so cloning is O(n) for n values in total. seen and the recursion use O(n) memory in the worst case. deepEqual also visits each value at most once per side, O(n).
What this simple version skips
It treats Date, Map, Set, and class instances as plain objects, which loses their type. The built-in structuredClone handles those, plus cycles, but it can't copy functions or DOM nodes. deepEqual here uses === for primitives, so NaN isn't equal to NaN; use Object.is if that matters. Comparing two cyclic structures needs a seen set of pairs, too.
Work it by hand
Every step in a table, with why
Clone a document whose owner is also its only editor, and whose owner points back at the document. Both repeats are found in seen.
ida = { name: "Ida", doc }, doc = { title: "Q3", owner: ida, editors: [ida] }; deepClone(doc)
| Step | Call | value | In seen? | seen.size | Returns | Why |
|---|---|---|---|---|---|---|
| 1 | deepClone(doc) | the doc object | no | 1 | doc copy (being filled) | Not a primitive and not seen, so make copy = {} and record doc before looking at any key. |
| 2 | .title | "Q3" | - | 1 | "Q3" | A string is a primitive, so it is returned as is. |
| 3 | .owner | the ida object | no | 2 | ida copy (being filled) | A new object: make an empty copy for ida and record it. |
| 4 | .owner.name | "Ida" | - | 2 | "Ida" | Another primitive. |
| 5 | .owner.doc | the doc object | yes | 2 | doc copy | The loop back to doc. It is already in seen, so the half-built copy is returned instead of starting again. |
| 6 | .editors | the editors array | no | 3 | new array (being filled) | Arrays get [] instead of {}, so Array.isArray stays true for the copy. |
| 7 | .editors[0] | the ida object | yes | 3 | ida copy | The same user again. seen returns the copy made at .owner, so the copy shares it too. |
| 8 | return | - | - | 3 | doc copy | Every key of doc is filled, so the top call returns its copy. |
3 new objects were made, one per original object. In the copy, copy.owner === copy.editors[0] and copy.owner.doc === copy are both true, just like in the original, while copy !== doc.
Compared with other ways
When to pick something else
| Approach | Time | Extra memory | Choose it when |
|---|---|---|---|
| Deep clone and deep equal | O(n) | O(n) | You need a fully separate copy of plain objects and arrays, including loops and shared parts, or you're asked to write it in an interview. |
structuredClone | O(n) | O(n) | In real code, when the data has no functions or DOM nodes. It handles more built-in types than a hand-written clone. |
JSON.parse(JSON.stringify(value)) | O(n) | O(n) | Plain JSON data only: strings, numbers, booleans, null, arrays, and objects, with no loops, dates, or undefined. |
Shallow copy { ...value } | O(k) | O(k) | You only change top-level keys, as in immutable updates. It copies the k top-level keys and shares everything below. |
Extra memory: O(n) for seen and the copy itself. The recursion uses one call per level of nesting, so a very deep chain uses a lot of call stack.
How the steps grow
- Deep clone and deep equal,
structuredClone,JSON.parse(JSON.stringify(value)), Shallow copy{ ...value }O(n)
| Approach | n = 10 | n = 1,000 | n = 1,000,000 |
|---|---|---|---|
| Deep clone and deep equal | 10 | 1,000 | 1 million |
structuredClone | 10 | 1,000 | 1 million |
JSON.parse(JSON.stringify(value)) | 10 | 1,000 | 1 million |
Shallow copy { ...value } | 10 | 1,000 | 1 million |
Inputs that need care
6 tricky inputs
- A
Dateor aMap:deepClone(new Date(0)) Object.keysof aDateor aMapis empty, so the copy is a plain{}and the value is lost. Checkinstanceof Date,Map, andSetfirst and rebuild them, or usestructuredClone.- Two different dates:
deepEqual(new Date(0), new Date(5000)) - Both have zero own keys, so it returns
true. ComparegetTime()for dates, and treat other built-in types the same way. - A sparse array:
[1, , 3] Object.keysskips the hole, but settingcopy[2]makes the copy's length 3, so the hole is kept: the copy is[1, , 3]too.- Symbol keys and class instances
Object.keysskips symbol keys, so{ [Symbol("s")]: 1, x: 2 }copies as{ x: 2 }. A class instance becomes a plain object without its methods, because the copy is made with{}.- A chain of objects thousands of levels deep
- Each level is one more recursive call. In a test with Node.js 22, a chain 5,000 objects deep already overflowed the call stack. An explicit stack of
[original, copy]pairs avoids that. - Two structures with loops:
x.self = x,y.self = y, thendeepEqual(x, y) deepEqualfollowsselfforever and throwsRangeError: Maximum call stack size exceeded. Add aseenmap of pairs already being compared, and treat a repeat pair as equal.
Variations and follow-ups
Twists interviewers add
structuredClone
Browsers and Node.js have structuredClone(value), which copies loops, shared objects, Date, Map, Set, RegExp, and typed arrays. It throws a DataCloneError for functions and DOM nodes, and class instances come back as plain objects. It's the same copying used when you postMessage data to a worker.
Keeping more types
Before the generic branch, check the type: new Date(value) for dates, a new Map with cloned keys and values, a new Set with cloned items. To keep a class's methods, make the copy with Object.create(Object.getPrototypeOf(value)) instead of {}.
Deep equal with loops
Pass a Map from objects in a to the objects in b they're being compared with. If the pair (a, b) is already there, return true: if they differ, the first comparison of that pair will find out. This lets deepEqual finish on structures that point back at themselves.
Copying only what changes
Immutable updates, as in Immer or Redux reducers, copy only the objects on the path to the change and share everything else. That's cheaper than a deep clone, and a shallow === check can then tell which parts changed.
In an interview
Spot it, explain it, answer follow-ups
- Spot it from "a copy that shares nothing with the original", "compare nested objects", "circular", or "cycle". If the data can point back at itself,
seenis the main point of the question. - Explain it in order: primitives first, then the
seencheck, then make and record the copy, then fill it. Say out loud why recording happens before filling. - Test by hand:
null, an array inside an object, a shared object used twice, and an object that points to itself. Check that the copy's shape matches, not just its values. - Expect the follow-ups "why not
JSON.stringify?" (it loses types and fails on loops), "supportMap,Set, andDate", and "makedeepEqualwork on loops" (aseenmap of pairs).
Where frontend code uses it
The same idea in real UI code
structuredClonein browsers and Node.js deep-clones values with cycles,Map,Set, andDate; it is also the algorithm used when youpostMessagedata to a worker.- React compares props and dependencies with
Object.is(shallow), not deep equality; libraries like Lodash'sisEqualand test matchers like Jest'stoEqualdo deep comparison. - State libraries that require immutable updates, like Redux, rely on copies so that changes create new objects and shallow checks can detect them.