← back to the section

"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: sort mutates — toSorted doesn't; push mutates — spread/concat don't.
  • 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.