Deep Clone Objects in JavaScript
Cloning an object in JavaScript creates a copy that is independent from the original. A shallow copy (spread operator or Object.assign) duplicates only the top level — nested objects still share references. A deep clone copies the entire structure so mutations to the clone do not affect the original. This guide covers four approaches:JSON.parse/JSON.stringify, structuredClone, Lodash cloneDeep, and a recursive function.
Inspect your JSON structure in Jsonic to understand nesting before cloning.
Open JSON FormatterThe shallow copy problem
The spread operator and Object.assign only copy top-level properties. Nested objects and arrays are still shared by reference between the original and the copy, which leads to unexpected mutations:
const original = {
name: "Alice",
address: { city: "London", zip: "SW1" },
scores: [95, 87],
};
// Shallow copy — nested objects still shared
const shallow = { ...original };
shallow.name = "Bob"; // ✅ safe — primitive
shallow.address.city = "NYC"; // ❌ mutates original too!
console.log(original.address.city); // "NYC" — unintended side effectBecause shallow.address and original.address point to the same object in memory, mutating one also mutates the other. A deep clone breaks that shared reference at every level of nesting.
JSON.parse(JSON.stringify()) — the classic approach
The oldest and most widely known deep-clone technique serializes the object to a JSON string and immediately parses it back. Every nested object and array is recreated as a new reference.
Key facts about this approach:
- Fastest for large plain JSON-compatible objects (uses native C++ JSON parsing)
- Drops:
undefinedvalues, functions,Symbolproperties, class prototypes - Converts
Dateto a string — not back to aDateinstance - Throws on circular references: "Converting circular structure to JSON"
const user = {
id: 1,
name: "Alice",
address: { city: "London" },
tags: ["admin", "user"],
};
// Deep clone via JSON roundtrip
const clone = JSON.parse(JSON.stringify(user));
clone.address.city = "NYC";
clone.tags.push("editor");
console.log(user.address.city); // "London" — original unchanged
console.log(user.tags); // ["admin", "user"] — unchanged
// Limitation: undefined, functions, and Dates
const problematic = {
fn: () => "hello", // dropped
undef: undefined, // dropped
date: new Date(), // becomes string: "2024-01-15T..."
};
const clone2 = JSON.parse(JSON.stringify(problematic));
console.log(clone2.fn); // undefined (dropped)
console.log(clone2.date); // string, not Date objectUse this approach when your object contains only JSON-compatible values: strings, numbers, booleans, null, plain objects, and arrays. It is the best choice for objects that originate from an API response or a JSON file.
structuredClone() — the modern native approach
structuredClone is a global function introduced in Node.js 17+ and modern browsers (Chrome 98+, Firefox 94+, Safari 15.4+). It implements the HTML Structured Clone Algorithm and handles many types that JSON cannot:
- Handles:
Date,Map,Set,ArrayBuffer,RegExp, typed arrays - Handles circular references without throwing
- Does NOT handle: functions, class instances with prototype methods, DOM nodes
- Throws
DataCloneErrorfor non-cloneable types (functions, symbols)
// Node.js 17+ / modern browsers
const original = {
name: "Alice",
created: new Date("2024-01-15"),
metadata: new Map([["role", "admin"]]),
scores: new Set([95, 87, 92]),
};
const clone = structuredClone(original);
clone.name = "Bob";
clone.created.setFullYear(2020);
clone.metadata.set("role", "viewer");
console.log(original.name); // "Alice"
console.log(original.created.getFullYear()); // 2024 — unchanged
console.log(original.metadata.get("role")); // "admin" — unchanged
// Circular reference handled!
const circular = { a: 1 };
circular.self = circular;
const cloned = structuredClone(circular);
console.log(cloned.self === cloned); // true (circular ref preserved)structuredClone is the recommended default for new code in modern environments. It is faster than Lodash because it is implemented natively, and it handles more types than JSON.parse/JSON.stringify.
Lodash _.cloneDeep()
Lodash's cloneDeep is the most battle-tested deep-clone utility in the JavaScript ecosystem. It handles Date, RegExp,Map, Set, Buffer, circular references, and more — all in pure JavaScript.
// npm install lodash
import cloneDeep from 'lodash/cloneDeep';
// or: const { cloneDeep } = require('lodash');
const original = {
date: new Date("2024-01-15"),
regex: /^[a-z]+$/i,
nested: { count: 0, items: [1, 2, 3] },
};
const clone = cloneDeep(original);
clone.nested.count = 99;
clone.nested.items.push(4);
console.log(original.nested.count); // 0
console.log(original.nested.items); // [1, 2, 3]
console.log(clone.date instanceof Date); // true — preserved as Date
console.log(clone.regex instanceof RegExp); // trueUse Lodash when you need maximum compatibility (Node.js older than 17, legacy browsers), when your objects contain class instances whose prototype you want to preserve, or when you are already using Lodash in the project. Import from lodash/cloneDeep rather than the whole package to keep bundle size small.
Manual recursive deep clone (no dependencies)
A hand-rolled recursive function gives you full control and zero dependencies. The function below handles Date, RegExp, arrays, and plain objects. Extend it for Map, Set, or other types as needed.
function deepClone(value) {
// Primitives and null — return as-is
if (value === null || typeof value !== "object") {
return value;
}
// Date — copy the timestamp
if (value instanceof Date) {
return new Date(value.getTime());
}
// RegExp — copy flags and source
if (value instanceof RegExp) {
return new RegExp(value.source, value.flags);
}
// Array — recursively clone each element
if (Array.isArray(value)) {
return value.map(deepClone);
}
// Plain object — recursively clone each property
const cloned = {};
for (const key of Object.keys(value)) {
cloned[key] = deepClone(value[key]);
}
return cloned;
}
// Usage
const original = { a: 1, b: { c: [2, 3] }, d: new Date() };
const clone = deepClone(original);
clone.b.c.push(4);
console.log(original.b.c); // [2, 3] — unchangedThis implementation intentionally skips functions and Symbol-keyed properties (matching JSON.parse/JSON.stringify behavior). To handleMap and Set, add branches for value instanceof Map and value instanceof Set before the plain-object fallthrough.
Comparison: four deep-clone methods
| Method | Handles Date/RegExp | Handles Map/Set | Circular refs | Functions | Performance |
|---|---|---|---|---|---|
JSON.parse/JSON.stringify | ❌ (Date → string) | ❌ | ❌ Throws | ❌ Dropped | Fast (native) |
structuredClone | ✅ | ✅ | ✅ | ❌ Throws | Fastest (native) |
Lodash cloneDeep | ✅ | ✅ | ✅ | ❌ Dropped | Medium |
| Recursive function | ✅ with custom logic | ❌ by default | ❌ by default | Configurable | Varies |
Frequently asked questions
What is the difference between shallow copy and deep copy?
A shallow copy duplicates only the top-level properties. If a property value is an object or array, both the original and the copy point to the same reference in memory — mutating one affects the other. A deep copy recursively duplicates all nested objects and arrays, so the clone is fully independent from the original.
What are the limitations of JSON.parse(JSON.stringify()) for deep cloning?
JSON serialization drops any property whose value is undefined, a function, or a Symbol. It converts Date objects to ISO strings (not back to Date instances). It cannot handle circular references — an object that references itself causes a "Converting circular structure to JSON" error. For objects without these cases, it is a fast and dependency-free approach.
When should I use structuredClone instead of JSON.parse/JSON.stringify?
Use structuredClone when your object contains Date, RegExp, Map, Set, ArrayBuffer, or other structured-cloneable types. structuredClone preserves these types natively and also handles circular references. It is available in Node.js 17+, Deno 1.14+, and all modern browsers (Chrome 98+, Firefox 94+, Safari 15.4+).
Does structuredClone clone functions or class instances?
No. structuredClone throws a DataCloneError if the object contains a function, a DOM node, or certain other non-cloneable types. Class instances are cloned as plain objects — the prototype chain is not preserved. If you need to clone class instances with methods, use a library like Lodash cloneDeep or write a custom clone function.
How do I deep clone an array in JavaScript?
All the same methods apply to arrays. JSON.parse(JSON.stringify(arr)) clones a JSON-compatible array. structuredClone(arr) clones an array including nested objects. For a flat array of primitives, the spread operator [...arr] or arr.slice() creates a sufficient copy since primitives are passed by value.
Is Lodash cloneDeep slower than JSON.parse/JSON.stringify?
For large plain objects, JSON.parse/JSON.stringify is typically faster because it uses native C++ JSON parsing. Lodash cloneDeep handles more edge cases (Date, RegExp, Map, Set, circular refs) but does so in JavaScript. structuredClone is the fastest overall for most cases in modern runtimes because it is implemented natively. Benchmark your specific payload rather than assuming.
Inspect and validate your JSON before deep cloning
Paste your JSON into Jsonic to check for syntax errors, inspect nested structure, and pretty-print deeply nested objects — so you know exactly what your clone function needs to handle.
Open JSON Formatter