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.

6 min read

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.

Shallow copy shares nested references

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:

javascriptjavascript
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:

javascriptjavascript
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:

javascriptjavascript
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:

javascriptjavascript
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.

structuredClone() creates a true deep copy. Every level of nesting is independently duplicated, starting with the same array of objects from the earlier example:

javascriptjavascript
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:

javascriptjavascript
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:

javascriptjavascript
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:

javascriptjavascript
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:

javascriptjavascript
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:

javascriptjavascript
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:

javascriptjavascript
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:

javascriptjavascript
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

MethodDepthHandles DatesHandles FunctionsHandles Circular RefsSpeed
[...arr]ShallowShared refShared refShared refFastest
structuredClone()DeepYesNo (throws)YesFast
JSON.parse(JSON.stringify())DeepStringifiesDropsNo (throws)Slower
Manual spread at each levelCustomManualManualNoDepends

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:

javascriptjavascript
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:

javascriptjavascript
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

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.
RunePowered by Rune AI

Frequently Asked Questions

When should I use structuredClone() instead of spread?

Use structuredClone() when your array or object contains nested objects or arrays that you need to independently modify. Spread is fine for flat arrays of primitives like numbers and strings.

What types can structuredClone() not copy?

structuredClone() cannot copy functions, DOM nodes, Error objects, WeakMaps, or symbols. It throws a DataCloneError if you try. For those cases, use a library like Lodash's cloneDeep or write a custom deep copy function.

Does JSON.parse(JSON.stringify(obj)) work for deep copying?

It works for plain data without functions, undefined values, Date objects, or circular references. However, structuredClone() is more capable and faster. Prefer structuredClone() for deep copies in modern JavaScript.

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.