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.
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 keys | New, independent | New, independent |
| Nested objects/arrays | Same reference — shared! | Recursively cloned |
| Circular references | N/A (not traversed) | Handled correctly |
| Functions | Reference copied | Throws (not cloneable) |
| Typical use | Adding/removing top-level props | Cloning deeply nested state safely |
Build It
Code Examples
The shallow-copy trap, and structuredClone as the fix
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 independentA minimal reactive object using Proxy + Reflect
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
- 1Write a function that proves { ...obj } leaks nested mutations, then fix it with structuredClone and re-verify.
- 2Extend the reactive() example to track WHICH property is being read inside a running "effect" function (a mini dependency tracker).
- 3Use Object.freeze on a nested object, attempt a deep mutation, and explain in writing why it succeeds despite the freeze.