Примитивы и ссылки в 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.

Частые вопросы

В чём разница между примитивом и ссылкой?
Примитив (число, строка, boolean) хранит само значение и копируется целиком. Объект и массив хранятся по ссылке: переменная держит адрес, а не сам объект. Присвоение копирует адрес, а не содержимое.
Почему изменение копии объекта меняет оригинал?
Потому что const copy = obj копирует ссылку, а не объект. Обе переменные указывают на один и тот же объект в памяти, поэтому изменение через любую из них видно везде. Для независимой копии нужен spread или structuredClone.
JavaScript передаёт аргументы по значению или по ссылке?
Всегда по значению, но для объектов это значение — ссылка. Поэтому функция может изменить свойства переданного объекта (видно снаружи), но не может переприсвоить саму переменную вызывающего кода.
Почему два одинаковых объекта не равны?
Оператор === сравнивает объекты по ссылке, а не по содержимому. {a:1} === {a:1} даёт false, потому что это два разных объекта в памяти. Примитивы же сравниваются по значению: 1 === 1 это true.
Как сделать настоящую копию объекта?
Поверхностную — spread { …obj } или Object.assign: копирует верхний уровень, вложенное общее. Глубокую — structuredClone(obj): копирует всю структуру целиком, включая вложенные объекты.