Deep Clone in JavaScript: structuredClone vs lodash vs JSON
Compare deep clone methods in JavaScript, Python and Java. Create independent copies of objects and arrays, handle circular references, Dates, Maps, Sets and typed arrays, and pick the right approach with a decision matrix.
Overview
A deep clone copies an object or array and everything it contains, so the clone is fully independent. Nested objects,
arrays and special types are cloned, not shared, so the copy doesn’t reference the original. In JavaScript, = only
copies the reference, so changes to a “copy” also change the original. This recipe compares deep clone methods in
JavaScript, Python and Java, including structuredClone, JSON.parse/stringify, Lodash and a manual recursive
implementation. Check the decision matrix in Variants to choose an approach. If you want a step-by-step
JavaScript-only walkthrough, see Deep Clone Structured. It also covers fallbacks for
older runtimes, so you don’t have to swap libraries mid-project.
When to Use
- Edit a nested state copy without touching the original, like in Redux reducers or form handling.
- You’re serializing objects to pass through
postMessage, IndexedDB, or Web Workers. - You’re building undo/redo stacks and need immutable snapshots.
- You want a defensive copy of function arguments or API responses before transforming them. See Call REST API for related patterns.
- Weigh JSON parsing trade-offs before copying a payload. See Parse JSON for
JSON.parseedge cases. - You need a mental model that transfers to backend code in Python or Java, so the whole stack uses the same cloning rules.
Solution
structuredClone (recommended)
const original = {
name: "Alice",
dates: [new Date("2024-01-01"), new Date("2024-06-01")],
map: new Map([["key", "value"]]),
set: new Set([1, 2, 3]),
buffer: new Uint8Array([1, 2, 3]),
nested: { a: 1, b: { c: 2 } },
};
const clone = structuredClone(original);
// Mutations don't affect the original
clone.nested.b.c = 999;
clone.dates[0] = new Date("2025-01-01");
console.log(original.nested.b.c); // 2
console.log(original.dates[0]); // 2024-01-01
// Circular references also work
const circular = { name: "self" };
circular.self = circular;
const circularClone = structuredClone(circular);
JSON.parse (quick but limited)
function jsonClone(obj) {
return JSON.parse(JSON.stringify(obj));
}
// Works for: plain objects, arrays, strings, numbers, booleans, null
// Loses: Dates (become strings), Functions, undefined, Maps, Sets, RegExp,
// circular references, and typed arrays
const limited = jsonClone({ a: 1, b: [2, 3], c: { d: 4 } });
Manual recursive clone with circular reference support
function deepClone(obj, cache = new WeakMap()) {
// Primitives and functions return as-is
if (obj === null || typeof obj !== "object") return obj;
// Circular reference
if (cache.has(obj)) return cache.get(obj);
// Date
if (obj instanceof Date) return new Date(obj.getTime());
// RegExp
if (obj instanceof RegExp) return new RegExp(obj.source, obj.flags);
// Map
if (obj instanceof Map) {
const copy = new Map();
cache.set(obj, copy);
obj.forEach((v, k) => copy.set(deepClone(k, cache), deepClone(v, cache)));
return copy;
}
// Set
if (obj instanceof Set) {
const copy = new Set();
cache.set(obj, copy);
obj.forEach((v) => copy.add(deepClone(v, cache)));
return copy;
}
// Typed arrays
if (ArrayBuffer.isView(obj)) {
const Constructor = obj.constructor;
return new Constructor(obj);
}
// Array
if (Array.isArray(obj)) {
const copy = [];
cache.set(obj, copy);
obj.forEach((v, i) => (copy[i] = deepClone(v, cache)));
return copy;
}
// Plain object (preserves prototype)
const copy = Object.create(Object.getPrototypeOf(obj));
cache.set(obj, copy);
Object.keys(obj).forEach((k) => (copy[k] = deepClone(obj[k], cache)));
Object.getOwnPropertySymbols(obj).forEach((s) => (copy[s] = deepClone(obj[s], cache)));
return copy;
}
// Usage
const obj = {
a: 1,
b: { c: 2 },
d: new Date("2024-01-01"),
e: new Map([["x", { y: 3 }]]),
f: [1, 2, { z: 4 }],
};
obj.circular = obj;
const cloned = deepClone(obj);
console.log(cloned.b === obj.b); // false
console.log(cloned.circular === obj); // false
console.log(cloned.circular === cloned); // true
Lodash
import cloneDeep from "lodash/cloneDeep.js";
const obj = {
a: 1,
b: { c: 2 },
d: new Date(),
e: new Map([["key", "value"]]),
f: new Uint8Array([1, 2, 3]),
};
const cloned = cloneDeep(obj);
// Handles circular refs, Dates, Maps, Sets, typed arrays, RegExp, plain objects, arrays
Python equivalent
import copy
from datetime import datetime
original = {
"name": "Alice",
"dates": [datetime(2024, 1, 1), datetime(2024, 6, 1)],
"nested": {"a": 1, "b": {"c": 2}}
}
cloned = copy.deepcopy(original)
cloned["nested"]["b"]["c"] = 999
print(original["nested"]["b"]["c"]) # 2
class Person:
def __init__(self, name):
self.name = name
self.friend = None
alice = Person("Alice")
bob = Person("Bob")
alice.friend = bob
cloned_alice = copy.deepcopy(alice)
print(cloned_alice.friend is bob) # False
print(cloned_alice.friend.name) # "Bob"
Java equivalent
import java.io.*;
import java.util.*;
public class DeepCopyUtil {
@SuppressWarnings("unchecked")
public static <T extends Serializable> T deepCopy(T obj) {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(obj);
oos.close();
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
T copy = (T) ois.readObject();
ois.close();
return copy;
} catch (IOException | ClassNotFoundException e) {
throw new RuntimeException("Deep copy failed", e);
}
}
}
public record Person(String name, List<Date> dates, Map<String, Object> metadata)
implements Serializable {}
Person original = new Person(
"Alice",
List.of(new Date(1704067200000L)),
new HashMap<>(Map.of("role", "admin"))
);
Person cloned = DeepCopyUtil.deepCopy(original);
// cloned is fully independent; mutations don't affect original
Explanation
- Choose
structuredClonewhen your runtime is modern. It’s native to browsers, Node 17+, Deno and Bun, and it handles circular references,Date,Map,Set, typed arrays and most built-in types. It throwsDataCloneErroron functions, DOM nodes and objects with prototype chains. MDN lists the supported types andDataCloneErrordetails forstructuredClone. JSON.parse(JSON.stringify(obj))is the fastest way to clone plain data, but it dropsundefined, functions,Map,Set,RegExp, typed arrays and circular references. It leavesDatevalues as ISO strings. Reserve it for plain objects and arrays. See Parse JSON forJSON.parseedge cases.- A manual
WeakMapcache gives you full control over how each type is cloned, including class instances rebuilt throughnew MyClass(...). UseWeakMap, notMap, so cached objects can still be garbage collected once the original is released. - Lodash
cloneDeepis the safest fallback whenstructuredCloneis missing or when you need to clone functions and accessors. The trade-off is bundle size: about 17 KB gzipped for all of Lodash or about 4 KB forlodash.cloneDeepalone. See the Lodash docs. - Java serialization and Python
copy.deepcopyfollow the same pattern: traverse the object graph, create new instances and point already-copied objects to the same new instance so circular references stay intact.
Where this recipe fits
Deep clone looks simple until a Date, a Map or a DOM node shows up in the payload. This page keeps the comparison
portable across JavaScript, Python and Java, with a decision tree for picking a method. The cross-language angle helps
when your team owns both a Node API and a Python data pipeline and needs the same cloning rules in each. If you only
care about JavaScript, Deep Clone Structured is the deeper, step-by-step guide. Use
that one when you want to build the clone by hand; use this one when you need the same mental model across a polyglot
stack.
Decision tree
Variants
| Approach | Circular refs | Special types | Performance | Environment |
|---|---|---|---|---|
structuredClone | Yes | Dates, Maps, Sets, typed arrays | Fast | Modern browsers, Node 17+ |
JSON.parse/stringify | No | None (Dates become strings) | Fastest | All environments |
| Manual recursion | Yes | Configurable | Medium | All environments |
Lodash cloneDeep | Yes | Dates, Maps, Sets, RegExp, etc. | Medium | All environments (requires dependency) |
| Java serialization | Yes | All Serializable types | Slow | Java JVM |
Python copy.deepcopy | Yes | Most built-in types | Medium | Python |
Environment matters as much as speed. structuredClone is useless in an old browser, and Lodash is overkill for a plain
config object. Match the method to the runtime and the shape of the data. If the payload is small and the runtime is
modern, structuredClone is usually the least surprising choice. You rarely need all of them at once.
Best Practices
- Prefer
structuredClonein modern environments. It’s native, avoids extra dependencies and covers circular references and most special types. - Use Lodash 4.x
cloneDeepwhen you still support older browsers or Node versions wherestructuredCloneisn’t available. - Keep
JSON.parse/stringifyaway from complex objects; it silently corruptsDate, functions,undefined,Map,Setand circular references. - Clone defensively before mutating API or state-store data, so you don’t introduce accidental side effects. See Call REST API for patterns on handling external responses.
- With very large immutable trees, libraries like Immer use structural sharing instead of copying the whole tree on every update.
- Write a small test that mutates the clone and checks the original is untouched. If you switch from
JSON.parse/stringifytostructuredClone, the supported types change, and a test catches that before it reaches production. - Revisit your clone strategy when you upgrade Node or your bundler. What needed Lodash last year may be native today, and removing that dependency shrinks your bundle.
Common Mistakes
- Using spread (
{...obj}) orObject.assignand expecting a deep copy. Spread copies the top level, yet nested objects still point to the original. - Cloning
Datevalues withJSON.parse/stringifyreturns ISO strings instead ofDateobjects. - Writing a manual deep clone without a cache and hitting infinite recursion or a stack overflow with circular references.
- Calling
structuredCloneon functions or DOM nodes. Functions and DOM nodes throw aDataCloneError. - Running a deep clone on a huge object on every render. That slows performance; use memoization or structural sharing instead.
- Treating a clone as immutable.
structuredClonegives you a snapshot, but the original reference can still live somewhere else, so the two copies can drift apart in ways that are hard to debug.
See Also
- MDN
structuredClone: supported types andDataCloneError. - Node.js
structuredCloneglobal: reference for Node 17+. - Node.js V8 serialization:
v8.serializeandv8.deserialize. - Lodash
cloneDeep: documentation and options. - More data recipes in the Data topic.
Frequently Asked Questions
Why does {...obj} not create a deep copy?
Spread syntax creates a shallow copy. It copies all enumerable own properties from obj into a new object, yet nested
objects and arrays still point to the original values. Use structuredClone, Lodash, or manual recursion for a true
deep copy.
Does structuredClone preserve class instances?
No. It strips prototype chains, so custom class instances become plain objects. If you need class behavior, rebuild
instances with new MyClass(...) or use a Lodash customizer. See
Prototype Pattern Cloning for an approach that preserves class behavior.
How do I deep clone in Node.js without dependencies?
Node 17.0 and later expose structuredClone globally. On older versions, v8.deserialize(v8.serialize(obj)) uses
Node's internal algorithm. Only reach for JSON.parse/stringify with simple plain objects. See
Node.js V8 serialization.
How do I clone objects with getters and setters?
Neither structuredClone nor JSON.parse/stringify keeps getters and setters. They evaluate the getter and copy the
resulting value. Use Object.getOwnPropertyDescriptors with Object.create to keep property descriptors and rebuild
the prototype chain. Lodash cloneDeep preserves accessors by default.
Why does JSON.parse(stringify(obj)) drop Date objects?
JSON.stringify doesn't know what a Date is. It sees a number, turns it into an ISO 8601 string, and by the time
JSON.parse reads it the type information is already gone. The parser only sees a string. Keep JSON.parse/stringify
for plain objects and arrays.
Related Resources
Parse JSON
How to parse JSON strings into native data structures across multiple programming languages.
RecipeFlatten and Unflatten Nested Objects
How to convert nested objects to flat key-value pairs and back again, with dot-notation, bracket notation, and custom separator support.
RecipeCall a REST API: Python, JavaScript, Java & Go Examples
How to make HTTP requests to a REST API and handle the JSON response in Python, JavaScript, Java, and Go.
RecipeDate Formatting
How to parse, format, and manipulate dates across timezones using Python, JavaScript, and Java.
PatternPrototype Pattern for Object Cloning and Configuration
Create new objects by copying existing ones, allowing pre-configured templates and avoiding subclass explosion when object creation is expensive
RecipeDeep Clone Objects in JavaScript: Beyond JSON.parse
Compare deep clone strategies including JSON.parse, structuredClone, manual recursion, and library approaches for copying nested objects with circular references and special types