«Скопировал объект, поменял копию — испортился оригинал» — возможно, самый массовый баг в JavaScript-коде. Его корень один: объекты живут по ссылке, и наивное «копирование» копирует ссылку, а не данные. Разберём, как копировать по-настоящему — и почему во фронтенде вместо изменения объектов их принято пересоздавать.
Присваивание — не копия
const user = { name: "Аня", tags: ["admin"] };
const copy = user; // скопирована ССЫЛКА
copy.name = "Боря";
user.name; // "Боря" — это один объект с двумя именами
То же самое происходит на каждом шагу неявно: объект, переданный в функцию, — та же ссылка; объект, положенный в массив, — ссылка; объект в состоянии React — ссылка. Функция, которая «немного подправила» аргумент, правит объект вызывающего кода — это источник самых дальнобойных багов: место поломки и место проявления разделены десятками файлов.
Сравнение тоже по ссылке:
{ a: 1 } === { a: 1 }; // false — разные объекты
const x = { a: 1 };
x === x; // true — та же ссылка
Оператор === для объектов отвечает на вопрос «это тот же самый объект?», а не «одинаковое ли содержимое?». Сравнения «по содержимому» в языке нет — его пишут руками или берут из библиотек.
Поверхностная копия: spread и его предел
const copy = { ...user }; // объект
const arr2 = [...arr]; // массив
const merged = { ...a, ...b }; // слияние: b перекрывает a
Spread копирует один уровень. Вложенные объекты остаются общими ссылками:
const copy = { ...user };
copy.name = "Боря"; // безопасно — верхний уровень скопирован
copy.tags.push("editor"); // ОПАСНО — tags общий!
user.tags; // ["admin", "editor"] — оригинал изменился
Это называется поверхностной (shallow) копией, и для плоских объектов её достаточно. Обновить вложенное без порчи оригинала — значит пересобрать каждый уровень на пути:
const updated = {
...user,
address: { ...user.address, city: "Казань" },
};
Глубокая копия: structuredClone
Когда нужна независимая копия всего дерева — structuredClone:
const deep = structuredClone(user);
deep.tags.push("editor");
user.tags; // ["admin"] — оригинал не тронут
Он справляется с вложенностью, датами, Map/Set и даже циклическими ссылками. Не умеет: функции и DOM-узлы (бросит ошибку). Старый трюк JSON.parse(JSON.stringify(x)) жив в легаси, но хуже по всем пунктам: теряет undefined, превращает даты в строки, падает на циклах.
Глубокая копия — не default, а инструмент по требованию: на больших структурах она дорогая. Обычный рабочий режим — поверхностная копия изменяемых уровней, как в примере с адресом.
Неизменяемость: почему React сравнивает ссылки
Во фронтенде господствует соглашение: не изменять объекты — создавать изменённые копии. Причина прагматичная: сравнить два дерева по содержимому дорого, а по ссылке — мгновенно. React (и не только) определяет «изменилось ли состояние» через ===:
// Мутация: ссылка та же — React не видит изменения, ререндера нет
state.items.push(newItem);
setState(state);
// Иммутабельно: новая ссылка — изменение видно
setState({ ...state, items: [...state.items, newItem] });
Отсюда же — частые методы-пары: изменяющие push/sort/splice против создающих concat/toSorted/slice. Современный JS добавил неизменяемые версии: toSorted, toReversed, with — сортировка без порчи оригинала теперь одна строчка.
Заморозить объект по-настоящему умеет Object.freeze(obj) — молча игнорирует записи (в строгом режиме — ошибка). Но он поверхностный, как spread, и в прикладном коде используется редко: соглашение «не мутируем» плюс TypeScript readonly работают дешевле.
Коротко
- Присваивание, передача в функцию, помещение в массив — везде копируется ссылка, не объект.
===сравнивает «тот же ли объект», а не содержимое. { ...obj }— копия одного уровня; вложенность остаётся общей. Обновление вложенного — пересборка каждого уровня на пути.- Глубокая копия —
structuredClone(не JSON-трюк). Дорого — использовать по необходимости. - Неизменяемость — про скорость сравнения: новая ссылка = сигнал «изменилось». Мутация состояния в React — невидимое изменение.
- Знать пары:
sortмутирует —toSortedнет;pushмутирует — spread/concatнет.
Что почитать дальше
- Типы в JavaScript — где начинается тема «значение против ссылки».
- Состояние: локальное и серверное — как неизменяемость работает в React-приложении.
- Промисы изнутри — следующая тема цикла.