Примитивы и ссылки в JavaScript: копирование
Содержание
О проверке кода. Примеры не запускались — в отличие от материалов по Node.js с фактическим выводом. Говорим прямо, а не ставим бейдж «проверено».
Это тема, на которой спотыкается почти каждый — и продолжает спотыкаться годами, ловя загадочные баги «я же скопировал, почему оригинал изменился?». Разберём её так, чтобы больше не возвращаться.
Два вида хранения
В JavaScript все типы данных делятся на две группы по тому, как они хранятся и копируются:
Примитивы — по значению
number, string, boolean, null, undefined, symbol, bigint
- Переменная хранит само значение
- Копия полностью независима
===сравнивает содержимое- Неизменяемы (immutable)
Ссылки — по адресу
object, array, function
- Переменная хранит адрес в памяти
- Копия ссылки → общий объект
===сравнивает адрес, не содержимое- Изменяемы (mutable)
Примитивы: копия независима
let a = 10;
let b = a; // копируется само значение
b = 20;
console.log(a); // 10 — a не тронуто
Примитив копируется целиком. b = a кладёт в b собственную копию числа 10. Дальше a и b живут отдельно — изменение одного не касается другого. С числами, строками, boolean интуиция работает правильно.
Объекты: копируется ссылка, не объект
const user = { name: 'Аня' };
const copy = user; // копируется АДРЕС, а не объект
copy.name = 'Борис';
console.log(user.name); // "Борис" — оригинал изменён!
Вот где ломается интуиция. const copy = user не создаёт новый объект — обе переменные держат адрес одного и того же объекта в памяти. Меняете через copy — видно через user, потому что это буквально тот же объект. Массивы работают точно так же.
user ─┐
├──► { name: "Борис" } (один объект в памяти)
copy ─┘
Это не баг и не странность — так устроены ссылочные типы во всех языках. Просто про это надо помнить.
Аргументы функций
Отсюда следует поведение при передаче в функции:
function rename(obj) {
obj.name = 'Борис'; // ✅ меняем свойство — видно снаружи
}
const user = { name: 'Аня' };
rename(user);
console.log(user.name); // "Борис"
function reassign(obj) {
obj = { name: 'Вера' }; // ❌ переприсваиваем переменную — снаружи НЕ видно
}
reassign(user);
console.log(user.name); // "Борис" — reassign ничего не изменил
JavaScript всегда передаёт аргументы по значению, но для объектов это «значение» — ссылка. Поэтому функция может изменить свойства переданного объекта (работает с тем же объектом), но не может переприсвоить переменную вызывающего кода — присваивание obj = ... меняет только локальную копию ссылки. Тонкое, но важное различие.
Сравнение: по значению и по ссылке
// примитивы — по значению
1 === 1; // true
'abc' === 'abc'; // true
// объекты — по ссылке
{ a: 1 } === { a: 1 }; // false — РАЗНЫЕ объекты, хоть и одинаковые на вид!
const x = { a: 1 };
const y = x;
x === y; // true — одна и та же ссылка
=== для объектов сравнивает адреса, а не содержимое. Два объекта с одинаковыми полями не равны — это разные объекты в памяти. Равны только две ссылки на один и тот же объект. Чтобы сравнить объекты по содержимому, пишут глубокое сравнение вручную или через библиотеку.
Как сделать настоящую копию
const user = { name: 'Аня', address: { city: 'Москва' } };
// поверхностная копия — верхний уровень независим
const shallow = { ...user };
shallow.name = 'Борис'; // ✅ оригинал цел
shallow.address.city = 'Казань'; // ❌ address ОБЩИЙ — оригинал изменится
// глубокая копия — независимо всё
const deep = structuredClone(user);
deep.address.city = 'Пермь'; // ✅ оригинал полностью цел
Поверхностная копия (spread или Object.assign) создаёт новый объект, но вложенные объекты остаются общими ссылками. Для большинства плоских объектов этого хватает.
Глубокая копия (structuredClone) дублирует всю структуру рекурсивно — оригинал и копия полностью независимы. Нужна, когда объект вложенный и вы собираетесь менять вложенное.
Почему это важно
Понимание примитивов и ссылок убирает целый класс багов:
- иммутабельные обновления в React/Redux — почему нельзя мутировать состояние, а надо создавать новое через spread;
constс объектом — почемуconst obj = {}позволяет менять свойства (защищена переменная, не содержимое);- общее состояние — почему передача объекта в функцию может неожиданно его изменить;
- сравнение — почему проверка «изменился ли объект» через
===со старым значением не работает после мутации.
Это не абстрактная теория, а причина конкретных ошибок в реальном коде.
Смежные темы: типы данных — что относится к примитивам; объекты — работа со ссылочными типами; spread и rest — копирование. Полный список — в уроках JavaScript.