Array/Object Copying

Course 2 · Ch 6
Array/Object Copying: Shallow vs Deep, and Mutation Pitfalls
Why spreading an object doesn't always actually copy it — the gap between "looks copied" and "is independent"

Intermediate Chapter 1 introduced the spread operator for copying arrays and objects, describing it as non-mutating, "the same spirit as map/filter." This chapter shows exactly where that breaks down: spread only copies one level deep, and anything nested still points at the original.

Reference Types: The Root of the Problem

const a = [1, 2, 3]; const b = a; // NOT a copy — b points at the SAME array as a b.push(4); console.log(a); // [1, 2, 3, 4] — a changed too!

Arrays and objects are reference types — a variable holding one doesn't hold the actual data, it holds a reference (an address) to where that data lives. const b = a copies the reference, not the array — both variables end up pointing at the exact same underlying array, so mutating through either one is visible through both.

Shallow Copying with Spread

const original = { name: "Philip", age: 35 }; const copy = { ...original }; copy.age = 36; console.log(original.age); // 35 — unaffected, this top level is a real copy

{ ...original } genuinely does create a separate object — for top-level, primitive values (strings, numbers, booleans) this is a complete, independent copy. This is "shallow" because it only copies one level deep; the moment a property's value is itself an object or array, the problem returns.

Where Shallow Copying Breaks Down

const original = { name: "Philip", address: { city: "London" } }; const copy = { ...original }; copy.address.city = "Paris"; console.log(original.address.city); // "Paris" — the ORIGINAL changed too!

Spread copied the address property, but that property's value is itself an object — spread copies the reference to that nested object, not the nested object's contents. copy.address and original.address still point at the exact same inner object, so mutating one mutates both. This is the most common real-world "spread didn't work" bug.

The same problem applies to arrays of objects
const copy = [...original] creates a new array, but if original contains objects, each element in copy still points at the SAME objects as original — changing a property on copy[0] changes original[0] too. Spreading an array is shallow in exactly the same way as spreading an object.

Deep Copying with structuredClone

const original = { name: "Philip", address: { city: "London" } }; const copy = structuredClone(original); copy.address.city = "Paris"; console.log(original.address.city); // "London" — genuinely independent now

structuredClone() is a built-in browser/Node function that performs a true deep copy — it recursively copies every nested object and array, so the result is completely independent of the original at every level, no matter how deeply nested.

structuredClone has limits
It can't clone functions or DOM elements (Fundamentals Chapter 8) — attempting to clone an object containing either throws an error. For ordinary data (objects, arrays, strings, numbers, dates, Maps, Sets), it's the standard modern tool, replacing older workarounds like JSON.parse(JSON.stringify(obj)), which silently loses things like undefined values and dates.

map() Avoids This Problem Entirely

const people = [ { name: "Alice", age: 28 }, { name: "Bob", age: 42 } ]; const updated = people.map(person => ({ ...person, age: person.age + 1 })); console.log(people[0].age); // 28 — original untouched console.log(updated[0].age); // 29

Combining map (Fundamentals Chapter 6) with object spread inside the callback creates a genuinely new object for each element — since name and age are both primitives here, this shallow approach is entirely sufficient; deep copying is only needed when the nested values are themselves objects/arrays that also need protecting from mutation.

OperationTop-level copy?Nested objects/arrays copied?
const b = aNo — same referenceNo
{ ...obj } / [...arr]Yes (shallow)No — nested values still shared
structuredClone(obj)YesYes — fully independent at every level

Coding Challenges

Challenge 1

Create const original = [1, 2, 3]. Create const sameRef = original (no copy) and const shallowCopy = [...original]. Push 4 onto sameRef and 5 onto shallowCopy, then log original, sameRef, and shallowCopy to show the different effects.

📄 View solution
Challenge 2

Create const settings = { theme: "dark", display: { brightness: 80 } }. Create a shallow copy with spread, change the copy's display.brightness to 50, then log the original's display.brightness to demonstrate the shallow-copy bug from this chapter.

📄 View solution
Challenge 3

Repeat Challenge 2, but use structuredClone instead of spread to create the copy. Change the copy's display.brightness to 50, then log the original's display.brightness to confirm it's now unaffected.

📄 View solution

Chapter 6 Quick Reference

  • const b = a (arrays/objects) — copies the reference only; both point at the same data
  • { ...obj } / [...arr] — shallow copy; top level is independent, nested values are NOT
  • structuredClone(obj) — true deep copy; every level is fully independent
  • structuredClone cannot clone functions or DOM elements — ordinary data only
  • map() + spread per item — a common, sufficient pattern when elements only hold primitives
  • Mutating a nested shared reference is the most common real-world "spread didn't work" bug
  • Next chapter: local storage and JSON — persisting data in the browser between page loads