A shallow copy can leave nested objects shared, while JSON serialization can turn a Date into a string and a Map into an empty object. This guide compares shallow copying, JSON-based cloning, and structuredClone() so you can choose a method that fits your data—and understand what each method cannot clone.
Key Takeaways
- For supported values in modern environments,
structuredClone()is usually the most direct way to deep-clone data that includesDate,Map, nested objects, or circular references. JSON.parse(JSON.stringify(value))only works reliably for JSON-compatible data; it does not preserve types such asDateandMapand fails on circular references.- Object spread and
Object.assign()make shallow copies. Nested objects remain shared, and spreading aDateorMapdoes not clone its internal data. structuredClone()cannot clone everything: functions and DOM nodes, for example, cause a cloning error. It also does not preserve custom prototypes.
What “deep clone” means
A shallow copy creates a new outer object but retains references to nested objects. A deep clone recursively duplicates supported data, so changing a nested value in the copy does not change the original.
Consider an object containing a nested user record, a Date, and a Map. A suitable clone should preserve those built-in types and avoid sharing the nested user object. Whether you also need to preserve class behavior, functions, or other special values affects which approach is appropriate.
Compare the three approaches
| Method | Nested objects | Date and Map | Circular references | Main limitation |
|---|---|---|---|---|
Object spread or Object.assign() | Shared, not cloned | Not cloned as built-in values | No recursive traversal | Shallow copy only |
| JSON stringify and parse | Recreated when JSON-compatible | Date becomes a string; Map typically becomes {} | Throws | Loses or rejects non-JSON values |
structuredClone() | Cloned when supported | Preserved | Supported | Rejects unsupported values and does not preserve custom prototypes |
Shallow copying with spread
Object spread copies an object's own enumerable properties into a new object. It does not recursively copy property values:
const original = {
profile: { name: "Rae" },
};
const copy = { ...original };
console.log(copy !== original); // true
console.log(copy.profile === original.profile); // true
copy.profile.name = "Sam";
console.log(original.profile.name); // "Sam"
Object.assign({}, original) has the same shallow-copy issue here. Spread and Object.assign() are useful when you want a new outer object but intend to keep nested references shared. They are not deep-cloning methods.
Deep cloning with structuredClone()
structuredClone() uses the structured clone algorithm. It handles many common data values, including nested objects, Date, Map, and circular references. It is available in modern browsers and supported JavaScript runtimes, but check the documentation for your target environment if compatibility matters.
const original = {
createdAt: new Date("2024-05-01T12:00:00.000Z"),
settings: {
theme: "dark",
},
scores: new Map([
["Ada", 10],
["Lin", 12],
]),
};
const copy = structuredClone(original);
console.log(copy !== original); // true
console.log(copy.settings !== original.settings); // true
console.log(copy.createdAt instanceof Date); // true
console.log(copy.createdAt.getTime() === original.createdAt.getTime()); // true
console.log(copy.scores instanceof Map); // true
console.log(copy.scores.get("Ada")); // 10
copy.settings.theme = "light";
copy.scores.set("Ada", 20);
console.log(original.settings.theme); // "dark"
console.log(original.scores.get("Ada")); // 10
The clone is independent for the data shown, while the Date and Map remain their respective built-in types. If your runtime might not provide the global function, check before calling it:
if (typeof globalThis.structuredClone !== "function") {
throw new Error("structuredClone is not available in this runtime");
}
This check detects missing support; it does not make unsupported values cloneable. When cloning fails because a value cannot be cloned, the operation throws rather than silently producing a partial clone.
Circular references
Unlike JSON serialization, structuredClone() can clone circular object graphs:
const original = { name: "loop" };
original.self = original;
const copy = structuredClone(original);
console.log(copy !== original); // true
console.log(copy.self === copy); // true
The clone's self property points to the clone, not to the original.
Unsupported values and prototype behavior
Functions and DOM nodes are examples of values that structuredClone() cannot clone. A function nested inside an otherwise ordinary object still makes that cloning attempt fail:
const value = {
count: 1,
run() {},
};
try {
structuredClone(value);
} catch (error) {
console.log(error.name); // commonly "DataCloneError" in browser environments
}
The precise error presentation can depend on the runtime. In browser code, attempting to clone a DOM node such as document.body also fails; use the DOM API intended for that node, such as cloneNode(), when a DOM copy is what you need.
structuredClone() does not preserve custom prototype chains. A cloned instance should not be assumed to retain its class methods or behave like an instance of the original custom class. It also does not preserve property descriptors or accessor behavior as a general-purpose object-copying mechanism. If your data depends on class invariants, methods, or custom construction, define an explicit conversion or cloning method for that type instead.
Why JSON serialization often breaks clones
The common pattern JSON.parse(JSON.stringify(value)) is only suitable when the value can safely be represented as JSON and the resulting JSON data is what you actually want.
const original = {
createdAt: new Date("2024-05-01T12:00:00.000Z"),
scores: new Map([["Ada", 10]]),
missing: undefined,
};
const copy = JSON.parse(JSON.stringify(original));
console.log(copy.createdAt); // "2024-05-01T12:00:00.000Z"
console.log(copy.createdAt instanceof Date); // false
console.log(copy.scores); // {}
console.log("missing" in copy); // false
JSON has no native representation for a Map, so the example's Map becomes an empty object. A Date is serialized as a string. Object properties with undefined values are omitted. JSON serialization also throws on circular references and on BigInt values without a custom serialization strategy. Other non-JSON values can be omitted, converted, or rejected depending on their type and position.
Use a JSON round trip only when you deliberately want JSON-compatible data and accept its conversion rules. A custom JSON replacer and reviver can handle particular types, but you must implement and maintain those rules yourself; that is not the same as a general deep clone.
Choosing the right approach
- Use
structuredClone()for supported data graphs where you need nested copies and wantDate,Map, or circular references preserved. - Use spread or
Object.assign()when only the outer object needs to be new and nested values should remain shared. - Use JSON serialization when you specifically need a JSON-compatible representation, such as preparing data for JSON transport—not as a universal cloning shortcut.
- Use type-specific logic when the value includes functions, DOM nodes, custom class behavior, or other values whose semantics need to be preserved explicitly.
For untrusted or complex data, also consider the cost of copying a large object graph. Cloning duplicates supported data and may consume substantial time and memory; it is not a substitute for validating inputs or limiting data size.
Common Mistakes
- Assuming a new outer object means a deep copy. Check whether nested references are identical when testing a clone.
- Using JSON serialization for
DateorMap. A date becomes a string, and a map does not become a cloned map. - Expecting
structuredClone()to preserve class instances. It is a data-cloning API, not a way to recreate arbitrary class behavior. - Cloning values containing functions without handling errors. Filter or transform unsupported values, or provide a type-specific strategy.
- Assuming every runtime supports the same global APIs. Verify availability in the browser or runtime versions you deploy to.
Best Practices
- Choose based on the values you need to preserve, not just the word “deep.”
- Add tests for identity and type: check that nested objects are distinct and that
DateandMapvalues retain their expected types. - Test failure paths with representative unsupported values, especially when data shapes can vary.
- Prefer explicit serialization formats for storage or network transfer. A clone is not automatically a portable or safe data format.
- For custom classes and DOM data, use APIs or explicit conversion logic designed for those types.
Conclusion
For nested data containing Date or Map, structuredClone() is generally the simplest built-in option when your runtime supports it and all values are cloneable. Use shallow copies when shared nested references are intentional, and reserve JSON round trips for data that is meant to follow JSON's conversion rules.
