Queries such as javascript deep copy, deep copy javascript, js deep copy, and deep copy object javascript usually mean either duplicating only the top-level container or duplicating nested objects and arrays so edits on the clone do not leak back to the original.
This guide walks through shallow versus deep behavior, the built-in structuredClone path, JSON and library options, and a small recursive helper for JSON-shaped data. The structuredClone API is available in modern browsers and in Node.js 17+ under the same global name.
Tested on: Node.js v20.18.2. A short note after each runnable snippet describes what you should see in the console.
Quick reference
| Goal | Typical API |
|---|---|
| Deep clone cloneable plain data (incl. cycles, Date, Map, Set) | structuredClone(value) |
| Deep clone JSON-only trees | JSON.parse(JSON.stringify(value)) |
| Deep clone with Lodash semantics / edge cases | cloneDeep from lodash |
| Copy top level only | { ...obj }, Object.assign({}, obj) |
Shallow vs deep (why spread is not enough)
- Shallow copy: only the first level is copied; nested objects and arrays remain shared references.
- Deep copy: every nested plain value you care about is duplicated, within the limits of the method you use.
Object spread is shallow for nested data:
const orig = { a: 1, nested: { x: 1 } };
const spr = { ...orig };
spr.nested.x = 99;
console.log(orig.nested.x, spr.nested.x);You should see one line with two 99 values—mutating the copy’s nested object changed the original too.
So deep copy in javascript for nested graphs is not solved by { ...obj } or Object.assign({}, obj) alone.
1. structuredClone() (default for cloneable data)
The global structuredClone function applies the structured clone algorithm (the same family as postMessage and IndexedDB serialization). For deep copy js scenarios where values are plain data, Dates, collections, and similar cloneable types—and no functions—it is usually the first API to try.
const original = {
n: [1, { y: 2 }],
d: new Date(0),
m: new Map([['a', 1]]),
};
const copy = structuredClone(original);
copy.n[1].y = 3;
console.log(original.n[1].y, copy.n[1].y);
console.log(copy.d instanceof Date, copy.d.getTime());
console.log(copy.m.get('a'));You should see three lines: nested y stays 2 on the original and 3 on the copy; copy.d is a real Date at epoch; the cloned Map still maps 'a' to 1.
Circular references, which break JSON.stringify, are preserved:
const circ = { a: 1 };
circ.self = circ;
const circ2 = structuredClone(circ);
console.log(circ2.self === circ2);You should see true—the clone’s self points back at the clone, not the original graph.
Typical failure: functions are not cloneable.
try {
structuredClone({ fn: () => {} });
} catch (e) {
console.log(e.name);
}You should see one line: DataCloneError.
Other limitations are spelled out in MDN’s structured clone algorithm notes: prototype chains and property descriptors are not preserved the way a hand-written copier might. For nested JSON workflows in your stack, see searching nested JSON.
Runtime: structuredClone exists in Node.js 17+ and current browsers; for older targets you may need a polyfill such as @ungap/structured-clone. Verify engine support before relying on it in production.
2. JSON.parse(JSON.stringify(value)) (plain JSON trees only)
Use this when the graph is small, acyclic, and JSON-safe: it is quick to write and needs no dependencies, but undefined keys disappear, Date / Map / Set / BigInt do not round-trip the way you might expect, and circular references throw.
const originalObject = {
name: 'Deepak',
age: 45,
hobbies: ['blogging', 'reading'],
};
const newObject = JSON.parse(JSON.stringify(originalObject));
newObject.name = 'Dave';
newObject.hobbies.push('drawing');
console.log(JSON.stringify(originalObject));
console.log(JSON.stringify(newObject));You should see two JSON strings: the first still shows Deepak with two hobbies; the second shows Dave with three hobbies—proving the top-level object was duplicated but the inner array was shared until you mutated through the copy.
undefined is dropped:
console.log(JSON.stringify(JSON.parse(JSON.stringify({ a: 1, u: undefined }))));You should see {"a":1}—the undefined value did not survive the round-trip.
Circular structures throw:
const circ = { a: 1 };
circ.self = circ;
try {
JSON.parse(JSON.stringify(circ));
} catch (e) {
console.log(e.name);
}You should see TypeError—JSON.stringify cannot serialize a cyclic graph.
3. Spread / Object.assign (shallow — know the limits)
These APIs copy top-level keys into a new object or array while keeping nested references shared. Use them when you intend to share nested state, or before handing values to a deep cloner—but they are not a javascript deep copy object solution on their own.
Top-level arrays of primitives still get a new array (each index holds a primitive value):
const originalArray = [13, 874, 192];
const newArray = [...originalArray];
console.log(JSON.stringify(newArray));
newArray[1] = 35;
console.log(JSON.stringify(originalArray));
console.log(JSON.stringify(newArray));You should see three JSON lines: first the cloned array, then the original unchanged after you only replaced index 1 on the copy, then the copy with 35 in the middle slot.
Object spread clones top-level keys only:
const originalObject = {
name: 'Femi',
skill: 'frontend',
language: 'javascript',
level: 2,
};
const newObject = { ...originalObject };
console.log(newObject);You should see one object log with the same four keys and values as the source.
Nested mutation with Object.assign:
const originalObject = {
name: 'Deepak',
age: 45,
hobbies: ['blogging', 'reading'],
};
const newObject = Object.assign({}, originalObject);
newObject.name = 'Dave';
newObject.hobbies.push('mutated-on-copy');
console.log(JSON.stringify(originalObject));
console.log(JSON.stringify(newObject));You should see two JSON strings that both list the extra hobby—Object.assign gave a new top-level object but the hobbies array is still one shared instance.
Notice the original hobbies array changed because the copy still points at the same array instance.
4. Lodash cloneDeep (libraries for tricky graphs)
Install Lodash when you need its cloning semantics (including some values structuredClone rejects) or you already depend on the library. See cloneDeep in the Lodash docs for full behavior.
npm install lodashconst { cloneDeep } = require('lodash');
const originalObject = {
name: 'Deepak',
age: 45,
hobbies: ['blogging', 'reading'],
};
const copyObject = cloneDeep(originalObject);
copyObject.hobbies.push('swimming');
console.log(JSON.stringify(originalObject.hobbies));
console.log(JSON.stringify(copyObject.hobbies));You should see the original hobbies unchanged in the first line, and the copy’s hobbies including swimming in the second.
5. jQuery $.extend(true, {}, obj) (legacy stacks only)
$.extend(true, target, source) recursively copies into target. It is mainly relevant for older codebases that already load jQuery; new projects should prefer structuredClone or small utilities.
The snippet below wires jsdom + jquery the way you might in a local Node script. It uses {run=false} because the hosted Run control may not install those packages.
const { JSDOM } = require('jsdom');
const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', {
url: 'http://localhost',
});
const { window } = dom;
const $ = require('jquery')(window);
const originalObject = {
name: 'Deepak',
age: 45,
hobbies: ['blogging', 'reading'],
};
const copyObject = $.extend(true, {}, originalObject);
copyObject.hobbies.push('swimming');
console.log(JSON.stringify(originalObject.hobbies));
console.log(JSON.stringify(copyObject.hobbies));You should see the same split as the Lodash example: only the deep-copied object’s array gained swimming.
6. Hand-written recursion (JSON-like objects only)
A tiny recursive function can walk plain objects and arrays without extra dependencies, but naive copy = {} logic breaks real arrays (push is missing). The version below handles arrays with Array.isArray; it still does not replace structuredClone for Map, Set, Date, cycles, or typed arrays.
function deepCopyJsonLike(value) {
if (value === null || typeof value !== 'object') return value;
if (Array.isArray(value)) return value.map(deepCopyJsonLike);
const out = {};
for (const key of Object.keys(value)) {
out[key] = deepCopyJsonLike(value[key]);
}
return out;
}
const originalObject = {
name: 'Deepak',
age: 45,
hobbies: ['blogging', 'reading'],
};
const copyObject = deepCopyJsonLike(originalObject);
copyObject.hobbies.push('coding');
console.log(JSON.stringify(originalObject.hobbies));
console.log(JSON.stringify(copyObject.hobbies));You should see the original hobbies untouched, then the copy with coding appended.
Summary
Start with structuredClone for plain cloneable data; fall back to JSON, Lodash, or hand recursion only when their trade-offs match your graph.
| Approach | Deep for nested plain data? | Cycles | Date / Map / Set |
Functions |
|---|---|---|---|---|
{ ...obj } / Object.assign |
No | N/A | references shared | copied by ref if value |
JSON round-trip |
Yes (JSON-safe only) | No | poor / lossy | dropped |
structuredClone |
Yes (cloneable types) | Yes | Yes | throws |
lodash.cloneDeep |
Yes (Lodash rules) | Yes | Yes | Yes |
For most javascript deep copy object work today, start with structuredClone. Use JSON only for strict JSON-shaped trees, and reach for Lodash or custom code when cloneability rules or prototypes require it.
References
- structuredClone() — MDN
- Structured clone algorithm — MDN
- Deep-copying in JavaScript using
structuredClone— web.dev - Object.assign() — MDN
- JSON.stringify() / JSON.parse() — MDN
- Lodash
cloneDeep
For how objects are normally written in JS, see object literals. For shallow array duplication patterns, see copy array and spread operator.
