"Copied an object, changed the copy — the original got corrupted" — possibly the most widespread bug in JavaScript code. Its root cause is single: objects live by reference, and naive "copying" copies the reference, not the data. Let's break down how to copy for real — and why in frontend code the convention is to recreate objects instead of mutating them.
Assignment is not a copy
const user = { name: "Anna", tags: ["admin"] };
const copy = user; // the REFERENCE was copied
copy.name = "Boris";
user.name; // "Boris" — it's one object with two names
The same thing happens implicitly at every turn: an object passed to a function — the same reference; an object put into an array — a reference; an object in React state — a reference. A function that "tweaked" its argument a bit edits the caller's object — that's the source of the longest-range bugs: the place of breakage and the place of manifestation are dozens of files apart.
Comparison is by reference too:
{ a: 1 } === { a: 1 }; // false — different objects
const x = { a: 1 };
x === x; // true — the same reference
For objects, the === operator answers "is this the same object?", not "is the content the same?". Comparison "by content" doesn't exist in the language — you write it by hand or take it from libraries.
Shallow copy: spread and its limit
const copy = { ...user }; // object
const arr2 = [...arr]; // array
const merged = { ...a, ...b }; // merge: b overrides a
Spread copies one level. Nested objects remain shared references:
const copy = { ...user };
copy.name = "Boris"; // safe — the top level was copied
copy.tags.push("editor"); // DANGEROUS — tags is shared!
user.tags; // ["admin", "editor"] — the original changed
This is called a shallow copy, and for flat objects it's enough. Updating something nested without corrupting the original means rebuilding every level along the path:
const updated = {
...user,
address: { ...user.address, city: "Kazan" },
};
Deep copy: structuredClone
When you need an independent copy of the whole tree — structuredClone:
const deep = structuredClone(user);
deep.tags.push("editor");
user.tags; // ["admin"] — the original is untouched
It handles nesting, dates, Map/Set, and even circular references. What it can't do: functions and DOM nodes (throws an error). The old trick JSON.parse(JSON.stringify(x)) lives on in legacy code but is worse on every count: it loses undefined, turns dates into strings, and blows up on cycles.
A deep copy is not the default but a tool used on demand: on large structures it's expensive. The usual working mode is a shallow copy of the levels being changed, as in the address example.
Immutability: why React compares references
Frontend is dominated by a convention: don't mutate objects — create modified copies. The reason is pragmatic: comparing two trees by content is expensive, while by reference it's instant. React (and not only React) determines "did the state change" via ===:
// Mutation: the reference is the same — React doesn't see the change, no re-render
state.items.push(newItem);
setState(state);
// Immutably: a new reference — the change is visible
setState({ ...state, items: [...state.items, newItem] });
Hence the frequent method pairs: mutating push/sort/splice versus creating concat/toSorted/slice. Modern JS added immutable versions: toSorted, toReversed, with — sorting without corrupting the original is now a one-liner.
Truly freezing an object is what Object.freeze(obj) does — it silently ignores writes (in strict mode — an error). But it's shallow, like spread, and is rarely used in application code: the "we don't mutate" convention plus TypeScript readonly are cheaper.
In short
- Assignment, passing to a function, putting into an array — everywhere the reference is copied, not the object.
===compares "is it the same object", not the content. { ...obj }is a one-level copy; nesting stays shared. Updating something nested means rebuilding every level along the path.- Deep copy —
structuredClone(not the JSON trick). Expensive — use when needed. - Immutability is about comparison speed: a new reference = the "it changed" signal. Mutating state in React is an invisible change.
- Know the pairs:
sortmutates —toSorteddoesn't;pushmutates — spread/concatdon't.
What to read next
- Types in JavaScript — where the "value versus reference" topic begins.
- State: local and server — how immutability works in a React application.
- Promises from the inside — the next topic of the series.