RoadmapDay 10 / 80
JavaScript CoreMonth 1 · Week 2

Day 10: Deep vs Shallow Copy, Object Mutability & Proxy/Reflect API

Know precisely which copy method solves which problem, and use Proxy/Reflect to understand how libraries like Vue and Immer intercept object access.

Mark this day complete

Study

Concepts

Shallow copy stops at the first level

Spread (`{ ...obj }`), `Object.assign({}, obj)`, and `Array.prototype.slice()` all create a NEW top-level container but copy nested objects/arrays by reference — mutating `copy.nested.value` still mutates the original's nested object too. This is the #1 source of "I copied it but it still changed!" bugs.

A true deep copy recursively clones every nested level. `structuredClone(obj)` (native, modern engines) is now the correct default — it handles circular references, Maps, Sets, Dates, and typed arrays, unlike the old `JSON.parse(JSON.stringify(obj))` hack, which silently drops functions/undefined/Symbols and throws on circular references.

Immutability is a convention JS does not enforce by default

`const` only prevents reassigning the binding, not mutating the object it points to. `Object.freeze(obj)` makes an object's OWN properties non-writable (shallow — nested objects are still mutable unless you freeze them too), and throws in strict mode on a failed write attempt.

Libraries like Immer use a Proxy internally to let you write "mutating" code (`draft.user.name = 'Ali'`) against a draft object, while actually recording the changes and producing a real, structurally-shared immutable object at the end — the mutation never touches the original.

Proxy + Reflect: programmable objects

A `Proxy` wraps a target object with a "handler" of traps (`get`, `set`, `has`, `deleteProperty`, etc.) that intercept fundamental operations. `Reflect` provides the default implementation of each trap, so you forward to `Reflect.get(target, prop, receiver)` inside your custom `get` trap instead of reimplementing default behavior — this is the idiomatic pairing.

This is exactly how Vue 3's reactivity system works: every reactive object is a Proxy whose `get` trap tracks "this property was read inside this effect" and whose `set` trap triggers "re-run effects that depend on this property" — no getter/setter boilerplate needed per property, unlike Vue 2's Object.defineProperty approach.

See It

Visualizations

Visualization

Shallow copy vs deep copy

 Shallow ({...obj})Deep (structuredClone)
Top-level keysNew, independentNew, independent
Nested objects/arraysSame reference — shared!Recursively cloned
Circular referencesN/A (not traversed)Handled correctly
FunctionsReference copiedThrows (not cloneable)
Typical useAdding/removing top-level propsCloning deeply nested state safely

Build It

Code Examples

The shallow-copy trap, and structuredClone as the fix

js
const original = { user: { name: 'Ali', prefs: { theme: 'dark' } } };

const shallow = { ...original };
shallow.user.prefs.theme = 'light';
console.log(original.user.prefs.theme); // 'light' — LEAKED into the "original"!

const deep = structuredClone(original);
deep.user.prefs.theme = 'blue';
console.log(original.user.prefs.theme); // still 'light' — fully independent

A minimal reactive object using Proxy + Reflect

js
function reactive(target, onChange) {
  return new Proxy(target, {
    get(obj, key, receiver) {
      const value = Reflect.get(obj, key, receiver);
      // Recursively wrap nested objects so deep mutations are tracked too
      return typeof value === 'object' && value !== null
        ? reactive(value, onChange)
        : value;
    },
    set(obj, key, value, receiver) {
      const ok = Reflect.set(obj, key, value, receiver);
      onChange(key, value); // this is the hook Vue calls "trigger"
      return ok;
    },
  });
}

const state = reactive({ count: 0, user: { name: 'Ali' } }, (key, value) => {
  console.log(`re-render triggered: ${key} -> ${value}`);
});

state.count = 1;          // "re-render triggered: count -> 1"
state.user.name = 'Sara'; // "re-render triggered: name -> Sara" (nested tracked too)

Remember

Key Takeaways

  • Spread/Object.assign are shallow — nested references are shared between "copies".
  • structuredClone is the modern, correct default for deep cloning (handles cycles, Maps, Sets, Dates).
  • Object.freeze is shallow and only prevents writes to top-level own properties.
  • Proxy traps (get/set/has/deleteProperty) intercept fundamental operations; Reflect gives you the default behavior to forward to.
  • Vue 3 reactivity and libraries like Immer are real production uses of exactly this Proxy+Reflect pattern.

Do It

Practice

  1. 1Write a function that proves { ...obj } leaks nested mutations, then fix it with structuredClone and re-verify.
  2. 2Extend the reactive() example to track WHICH property is being read inside a running "effect" function (a mini dependency tracker).
  3. 3Use Object.freeze on a nested object, attempt a deep mutation, and explain in writing why it succeeds despite the freeze.