Copying Nested Objects with the JS Spread Operator
The spread operator creates shallow copies. Learn what that means for nested objects and arrays, how to avoid shared reference bugs, and when to use structuredClone.
The spread operator ... creates a shallow copy. This means it copies the top-level elements, but if any element is itself an object or array, the copy shares a reference to that same inner object.
Changing the inner object in the copy changes it in the original too.
Both the original and the copy point to the same two inner objects, Alice and Bob in the diagram above. Only the outer array container is new.
The Problem: Shared References
Here is the bug in action. Start with an array of objects and make a shallow copy the normal way, with spread:
const original = [
{ name: "Alice", score: 85 },
{ name: "Bob", score: 92 }
];
const copy = [...original];Now change a score on the copy only, leaving the original array completely alone as far as the code is concerned, and check both arrays afterward:
copy[0].score = 100;
console.log(copy[0].score); // 100
console.log(original[0].score); // 100 (also changed!)You only modified copy, but original changed too. This happens because copy[0] and original[0] are the exact same object in memory. The spread copied the reference, not the object itself.
The same issue occurs with nested arrays:
const matrix = [[1, 2], [3, 4]];
const copy = [...matrix];
copy[0].push(99);
console.log(matrix); // [[1, 2, 99], [3, 4]]
console.log(copy); // [[1, 2, 99], [3, 4]]When Spread Is Safe
Spread is perfectly safe when the array contains only primitive values, numbers, strings, booleans, null, and undefined:
const numbers = [1, 2, 3];
const copy = [...numbers];
copy.push(4);
console.log(numbers); // [1, 2, 3] (unchanged)
console.log(copy); // [1, 2, 3, 4]Primitives are copied by value, not by reference. Each copy gets its own independent value. For these flat arrays, spread is the right choice -- it is fast and simple.
Solution 1: structuredClone() (Recommended)
structuredClone() creates a true deep copy. Every level of nesting is independently duplicated, starting with the same array of objects from the earlier example:
const original = [
{ name: "Alice", score: 85 },
{ name: "Bob", score: 92 }
];
const copy = structuredClone(original);Changing the copy this time leaves the original untouched, because each nested object was fully duplicated instead of just referenced:
copy[0].score = 100;
console.log(copy[0].score); // 100
console.log(original[0].score); // 85 (unchanged!)Beyond plain objects and arrays, structuredClone() also handles types that spread and JSON methods cannot, such as Dates, Sets, and Maps nested inside each other:
const data = {
date: new Date(),
items: new Set([1, 2, 3]),
nested: { values: new Map([["key", "value"]]) }
};
const clone = structuredClone(data);Changing a value inside the clone's nested Map leaves the original data completely untouched, since every level was independently duplicated:
clone.nested.values.set("key", "changed");
console.log(data.nested.values.get("key")); // "value" (unchanged)structuredClone() works with Dates, Maps, Sets, ArrayBuffers, RegExp, Blob, and circular references. It cannot clone functions, DOM nodes, or Error objects.
structuredClone() is available in all modern browsers and Node.js 17+. No library needed.
Solution 2: Manual Spread at Each Level
If you cannot use structuredClone() and have a known structure, you can spread at each nesting level instead. Mapping over the array and spreading both the object and its nested array produces independent copies at every level:
const original = [
{ name: "Alice", scores: [85, 90] },
{ name: "Bob", scores: [92, 88] }
];
const copy = original.map(item => ({
...item,
scores: [...item.scores]
}));Pushing a new score onto the copy now leaves the original array of scores unchanged, since scores was spread into a fresh array rather than shared by reference:
copy[0].scores.push(100);
console.log(original[0].scores); // [85, 90] (unchanged)
console.log(copy[0].scores); // [85, 90, 100]This works but is tedious for deeply nested data. You must manually spread at every level where you need independence. For more complex structures, structuredClone() is simpler and less error-prone.
Solution 3: JSON Methods (Limited)
JSON.parse(JSON.stringify(obj)) creates a deep copy but with serious limitations:
const original = [{ name: "Alice" }, { name: "Bob" }];
const copy = JSON.parse(JSON.stringify(original));
copy[0].name = "Changed";
console.log(original[0].name); // "Alice" (unchanged, works here)But it silently drops or corrupts unsupported types, which makes it risky for anything beyond plain, JSON-shaped data with no functions or special objects inside:
const data = {
date: new Date(),
fn: () => "hello",
value: undefined
};
const copy = JSON.parse(JSON.stringify(data));
console.log(copy); // { date: "2026-07-15T..." } (fn and undefined are gone!)Functions disappear, undefined disappears, and Dates become plain strings.
Use JSON methods only for plain JSON-shaped data with no special types.
Comparison of Copy Methods
| Method | Depth | Handles Dates | Handles Functions | Handles Circular Refs | Speed |
|---|---|---|---|---|---|
[...arr] | Shallow | Shared ref | Shared ref | Shared ref | Fastest |
structuredClone() | Deep | Yes | No (throws) | Yes | Fast |
JSON.parse(JSON.stringify()) | Deep | Stringifies | Drops | No (throws) | Slower |
| Manual spread at each level | Custom | Manual | Manual | No | Depends |
For flat arrays of primitives, use spread. For arrays with nested objects, use structuredClone(). For arrays with functions or DOM nodes, use manual copying or a library.
Practical Pattern: Immutable Updates
A common pattern is using spread for the outer copy and then selectively updating nested fields. Mapping over the array and spreading only the matching user creates a new object for the one item that changed:
const users = [
{ id: 1, name: "Alice", active: false },
{ id: 2, name: "Bob", active: true }
];
const updated = users.map(user =>
user.id === 1 ? { ...user, active: !user.active } : user
);Every other user in the array is reused as-is, while only the matching one gets a new object, so checking both arrays shows the original is untouched:
console.log(users[0].active); // false (original unchanged)
console.log(updated[0].active); // true (new object with change)This combines the spread operator with map() to create a new array where only the changed objects are replaced. It is the foundation of immutable state updates in React and other frameworks.
For more on array iteration with map(), see JS Array map vs forEach: Which Should You Use?. To understand how spread itself works with arrays, start with JS Spread Operator for Arrays.
Rune AI
Key Insights
- The spread operator creates a shallow copy: top-level primitives are copied, but nested objects share references.
- Modifying a nested object in a spread copy also changes the original.
- structuredClone() creates a true deep copy where nested objects are fully independent.
- structuredClone() handles Dates, Maps, Sets, and circular references, unlike JSON methods.
- For flat arrays of primitives (numbers, strings, booleans), spread is fast and sufficient.
Frequently Asked Questions
When should I use structuredClone() instead of spread?
What types can structuredClone() not copy?
Does JSON.parse(JSON.stringify(obj)) work for deep copying?
Conclusion
The spread operator is perfect for copying flat arrays of primitives. The moment you have nested objects or arrays, those inner references are shared between the original and the copy. For true independence, use structuredClone(). Understanding the difference between shallow and deep copies is essential for avoiding one of the most common and confusing bugs in JavaScript.
More in this topic
JavaScript While Loop Explained: A Complete Guide
A while loop repeats code as long as a condition stays true. Learn the syntax, see practical examples, and understand when a while loop is the right choice over a for loop.
JavaScript Loops Tutorial: for, while, do while
Loops let JavaScript repeat code without writing it twice. Learn the three core loop types, what each one does, and when to choose one over another.
How to Loop Through Arrays Using JS for Loops Guide
Looping through arrays is the most practical use of JavaScript for loops. Learn how to access each element by index, avoid off-by-one errors, and choose between for, for...of, and forEach.