Copying Nested Objects With the JS Spread Operator
A deep dive into copying nested objects with the JavaScript spread operator. Covers shallow vs deep copy behavior, why nested references are shared, manual deep-copy patterns, structuredClone, JSON round-trip limitations, recursive cloning, and library-based solutions for production code.
The spread operator (...) is the standard tool for cloning objects and arrays in JavaScript. But it produces a shallow copy, meaning nested objects inside the copy still point to the same memory as the original. This guide explains exactly what that means and shows every approach to safely copying deeply nested data.
The Shallow Copy Problem
const original = {
name: "Alice",
address: {
city: "Portland",
zip: "97201",
},
hobbies: ["reading", "hiking"],
};
const copy = { ...original };
// Top-level primitive is independent
copy.name = "Bob";
console.log(original.name); // "Alice" (safe)
// Nested object is shared
copy.address.city = "Seattle";
console.log(original.address.city); // "Seattle" (mutated!)
// Nested array is shared
copy.hobbies.push("cycling");
console.log(original.hobbies); // ["reading", "hiking", "cycling"] (mutated!)The spread operator copies property values one level deep. If a value is a reference (object, array, function), only the reference is copied, not the data it points to.
Visualizing Shallow vs Deep Copy
| Copy Type | Primitives | Nested Objects | Nested Arrays |
|---|---|---|---|
Shallow ({ ...obj }) | Independent copy | Shared reference | Shared reference |
Deep (structuredClone) | Independent copy | Independent copy | Independent copy |
Manual Nested Spread
For objects with known, limited nesting, you can spread at each level:
const original = {
name: "Alice",
address: {
city: "Portland",
zip: "97201",
},
hobbies: ["reading", "hiking"],
};
const deepCopy = {
...original,
address: { ...original.address },
hobbies: [...original.hobbies],
};
deepCopy.address.city = "Seattle";
console.log(original.address.city); // "Portland" (safe)
deepCopy.hobbies.push("cycling");
console.log(original.hobbies); // ["reading", "hiking"] (safe)Deeper Nesting Requires More Spreading
const state = {
user: {
profile: {
name: "Alice",
settings: {
theme: "dark",
notifications: { email: true, push: false },
},
},
},
};
// Updating notifications.push to true — immutably
const updated = {
...state,
user: {
...state.user,
profile: {
...state.user.profile,
settings: {
...state.user.profile.settings,
notifications: {
...state.user.profile.settings.notifications,
push: true,
},
},
},
},
};This works but becomes verbose quickly. For state management frameworks, libraries like Immer exist specifically to solve this verbosity.
structuredClone (Modern Deep Copy)
The structuredClone() function, available in all modern environments (browsers, Node 17+, Deno, Bun), creates a true deep copy:
const original = {
name: "Alice",
address: { city: "Portland" },
scores: [90, 85],
createdAt: new Date("2026-01-01"),
};
const deep = structuredClone(original);
deep.address.city = "Seattle";
deep.scores.push(100);
console.log(original.address.city); // "Portland" (safe)
console.log(original.scores); // [90, 85] (safe)
console.log(deep.createdAt instanceof Date); // true (preserved)What structuredClone Handles
| Type | Supported? |
|---|---|
| Plain objects, arrays | Yes |
| Date, RegExp, Map, Set | Yes |
| ArrayBuffer, TypedArrays | Yes |
| Blob, File, ImageData | Yes |
| Functions | No (throws) |
| DOM nodes | No (throws) |
| Symbols as values | No (throws) |
| Prototype chain | No (becomes plain object) |
JSON Round-Trip
The JSON.parse(JSON.stringify(obj)) pattern was the go-to deep copy before structuredClone:
const original = { name: "Alice", address: { city: "Portland" } };
const deep = JSON.parse(JSON.stringify(original));
deep.address.city = "Seattle";
console.log(original.address.city); // "Portland" (safe)JSON Round-Trip Pitfalls
const data = {
date: new Date("2026-01-01"),
pattern: /hello/gi,
nothing: undefined,
fn: () => "hello",
inf: Infinity,
nan: NaN,
map: new Map([["a", 1]]),
};
const clone = JSON.parse(JSON.stringify(data));
console.log(typeof clone.date); // "string" (Date becomes ISO string)
console.log(clone.pattern); // {} (RegExp becomes empty object)
console.log("nothing" in clone); // false (undefined properties dropped)
console.log("fn" in clone); // false (functions dropped)
console.log(clone.inf); // null (Infinity becomes null)
console.log(clone.nan); // null (NaN becomes null)
console.log(clone.map); // {} (Map becomes empty object)Use structuredClone() instead unless you specifically need JSON serialization.
Recursive Deep Clone Function
For environments without structuredClone or for educational purposes:
function deepClone(value) {
if (value === null || typeof value !== "object") {
return value;
}
if (value instanceof Date) return new Date(value.getTime());
if (value instanceof RegExp) return new RegExp(value.source, value.flags);
if (value instanceof Map) return new Map([...value].map(([k, v]) => [deepClone(k), deepClone(v)]));
if (value instanceof Set) return new Set([...value].map(deepClone));
if (Array.isArray(value)) {
return value.map(deepClone);
}
const result = {};
for (const [key, val] of Object.entries(value)) {
result[key] = deepClone(val);
}
return result;
}This handles common types but does not handle circular references. For production code, prefer structuredClone().
Immutable Update Patterns
In state management, you often need to update one nested property without mutating the original. Spread-based immutable updates are the standard:
// State shape
const state = {
todos: [
{ id: 1, text: "Buy milk", done: false },
{ id: 2, text: "Walk dog", done: true },
],
filter: "all",
};
// Toggle todo with id=1 — immutably
const newState = {
...state,
todos: state.todos.map(todo =>
todo.id === 1 ? { ...todo, done: !todo.done } : todo
),
};Each level that changes gets a new spread copy. Levels that do not change keep their references, which is efficient for equality checks.
When to Use Each Approach
| Scenario | Best Approach |
|---|---|
| One-level flat object | { ...obj } |
| Known shallow nesting (2-3 levels) | Manual nested spread |
| Arbitrary or unknown depth | structuredClone() |
| Serializable data only | JSON.parse(JSON.stringify()) |
| State management updates | Spread + map (or Immer) |
| Legacy environments (no structuredClone) | Recursive function or lodash cloneDeep |
Rune AI
Key Insights
- Spread creates shallow copies only: Nested objects and arrays are shared by reference between original and copy
- Manual nested spread works for known shapes: Spread at each level that contains objects, but it becomes verbose past 2-3 levels
- structuredClone is the modern deep copy: Handles Date, Map, Set, circular references, and arbitrary depth without gotchas
- JSON round-trip loses type information: Dates become strings, functions are dropped, undefined is stripped, and special numbers become null
- Immutable updates use spread plus map: Each changed level gets a new spread copy while unchanged references are preserved for efficient equality checks
Frequently Asked Questions
Does spread copy Symbol-keyed properties?
Is structuredClone slower than spread?
Can I deep-copy an object with circular references?
Does spread preserve getters and setters?
What about frozen or sealed objects?
Conclusion
The spread operator creates shallow copies only. Nested objects and arrays remain shared between original and copy. For most modern code, use structuredClone() for true deep copies. For immutable state updates in frameworks, manual nested spread is the standard convention. For more on the spread vs rest distinction, see JS spread vs rest operator complete tutorial. For related variable extraction techniques, see advanced array and object destructuring guide.
More in this topic
OffscreenCanvas API in JS for UI Performance
Master the OffscreenCanvas API to offload rendering from the main thread. Covers worker-based 2D and WebGL rendering, animation loops inside workers, bitmap transfer, double buffering, chart rendering pipelines, image processing, and performance measurement strategies.
Advanced Web Workers for High Performance JS
Master Web Workers for truly parallel JavaScript execution. Covers dedicated and shared workers, structured cloning, transferable objects, SharedArrayBuffer with Atomics, worker pools, task scheduling, Comlink RPC patterns, module workers, and performance profiling strategies.
JavaScript Macros and Abstract Code Generation
Master JavaScript code generation techniques for compile-time and runtime metaprogramming. Covers AST manipulation, Babel plugin authorship, tagged template literals as macros, code generation pipelines, source-to-source transformation, compile-time evaluation, and safe eval alternatives.