W3docs

Ссылки на объекты и копирование в JavaScript

Как JavaScript хранит объекты по ссылке, как клонировать с Object.assign и spread, и как безопасно выполнять глубокое копирование через structuredClone().

Одно из важнейших различий в JavaScript — то, как он обращается с примитивными значениями и объектами. Примитивы копируются «целым значением», а объекты хранятся и копируются «по ссылке». Непонимание этого единственного правила — источник бесчисленного множества ошибок, поэтому данное руководство подробно объясняет, что происходит в памяти, как сравнивать и клонировать объекты и как безопасно их копировать.

Примитивы копируются по значению

Примитив — string, number, boolean, null, undefined, bigint или symbol — копируется как полное, независимое значение. Присваивание одной переменной другой дублирует значение, после чего две переменные полностью независимы друг от друга.

let message = "Hello";
let phrase = message; // a full copy of the string is made

phrase = "Goodbye";

console.log(message); // "Hello"  — unaffected
console.log(phrase);  // "Goodbye"

Изменение phrase никак не влияет на message. В памяти хранятся две независимые строки.

Объекты хранятся и копируются по ссылке

Объекты работают иначе. Переменная, которой присвоен object, содержит не сам object, а ссылку (указатель) на место в памяти, где этот object находится. Копирование такой переменной копирует ссылку, а не object. Обе переменные указывают на один и тот же object.

javascript— editable

Объект по-прежнему один. Просто есть две переменные — user и admin — обе ссылаются на него. Изменение, сделанное через одну из них, видно через другую, потому что они описывают одно и то же.

Сравнение по ссылке

Две объектные переменные равны при сравнении == или === только тогда, когда они ссылаются на один и тот же object. Два независимых объекта никогда не равны, даже если их содержимое выглядит идентично.

javascript— editable

Это сделано намеренно: === для объектов спрашивает «являются ли они одним и тем же объектом?», а не «содержат ли они одинаковые данные?». Для сравнения содержимого нужен другой подход — например, сравнение свойств по одному или сериализация через JSON.

Объекты с const всё равно можно изменять

Распространённый сюрприз: объект, объявленный с const, всё равно позволяет изменять свои свойства. const фиксирует привязку — переменной нельзя присвоить другое значение — но не фиксирует содержимое объекта.

const user = { name: 'John' };

user.name = 'Pete'; // OK — we are mutating the object, not reassigning the variable
console.log(user.name); // 'Pete'

// user = { name: 'Alice' }; // TypeError: Assignment to constant variable

Первое изменение работает, потому что user по-прежнему ссылается на тот же object. Переприсваивание не работает, потому что тогда user указывал бы на совершенно новый object, что запрещает const. (Чтобы заморозить содержимое тоже, используйте Object.freeze().)

Поверхностное клонирование

Что если вам действительно нужна отдельная копия объекта? Нужно создать новый object и скопировать в него существующие свойства. Два современных и лаконичных способа сделать это — Object.assign и синтаксис spread.

Object.assign(target, ...sources) копирует все перечислимые собственные свойства из источников в цель и возвращает цель:

javascript— editable

Синтаксис spread {...obj} делает то же самое в ещё более краткой форме (см. остаточные параметры и синтаксис spread):

let user = { name: 'John', age: 30 };

let clone = { ...user };       // a shallow copy
let merged = { ...user, age: 31 }; // copy, then override age

console.log(clone);  // { name: 'John', age: 30 }
console.log(merged); // { name: 'John', age: 31 }

Оба способа создают настоящую, независимую копию — но только на верхнем уровне. Эта оговорка важна, и она подводит к следующему разделу.

Проблема вложенных объектов

Поверхностная копия дублирует свойства верхнего уровня. Но если значение свойства само является объектом или array, копируется только ссылка на этот вложенный object — сам вложенный object не копируется. Клон и оригинал в таком случае разделяют один и тот же вложенный object.

javascript— editable

Несмотря на то что clone — отдельный object верхнего уровня, clone.sizes и user.sizes указывают на один и тот же вложенный object. Изменение через любой из путей влияет на оба. Именно такая случайная ошибка с общей ссылкой и подстерегает тех, кто считает копирование через spread «полностью независимым».

Внимание

Spread ({...obj}) и Object.assign копируют только один уровень вглубь. Если ваш object содержит вложенные объекты или array, копия разделяет эти вложенные значения с оригиналом — изменение вложенного свойства через одну переменную незаметно изменит его в другой. Для вложенных данных используйте глубокое клонирование.

Глубокое клонирование с structuredClone()

Чтобы скопировать object вместе со всем, что вложено в него, используйте встроенный structuredClone(). Он рекурсивно клонирует вложенные объекты и массивы, поэтому результат полностью независим от оригинала.

javascript— editable

structuredClone также обрабатывает циклические ссылки (object, который прямо или косвенно ссылается сам на себя) без сбоев:

let user = {};
user.self = user; // user references itself

let clone = structuredClone(user);

console.log(clone === clone.self); // true — the cycle is preserved correctly

Помимо обычных объектов и массивов, он поддерживает многие встроенные типы, включая Date, Map, Set, RegExp, ArrayBuffer и типизированные массивы — точно копируя их значения, не превращая в нечто иное.

Ограничения structuredClone

structuredClone мощный, но не универсальный инструмент:

  • Он не может клонировать функции — при попытке выбрасывается DataCloneError. То же самое относится к DOM-узлам.
  • Он не копирует свойства с ключами-symbol, геттеры/сеттеры свойств и цепочку прототипов объекта — всё это просто убирается из результата.
// This throws DataCloneError because functions can't be cloned:
// structuredClone({ run: () => {} });

Если вам нужно скопировать функции или экземпляры классов с их методами, structuredClone не подходит — придётся написать собственный клон или воспользоваться библиотекой, например cloneDeep из lodash.

Старый трюк с JSON

До того как structuredClone стал широко доступен, популярным способом глубокого клонирования был трюк с сериализацией объекта в строку JSON и последующим разбором:

let user = { name: 'John', sizes: { height: 182, width: 50 } };

let clone = JSON.parse(JSON.stringify(user)); // deep copy via JSON

clone.sizes.width = 60;
console.log(user.sizes.width); // 50 — original unaffected

Это работает для простых, JSON-совместимых данных, но имеет реальные недостатки:

  • Функции и undefined теряются — ключи, содержащие их, просто исчезают.
  • Объекты Date превращаются в строкиJSON.stringify преобразует дату в ISO-строку, а при разборе она не восстанавливается.
  • Map, Set и другие специальные типы теряются или становятся пустыми объектами.
  • Циклические ссылки выбрасывают TypeError.
Информация

Для глубокого клонирования отдавайте предпочтение structuredClone() вместо трюка JSON.parse(JSON.stringify(...)). Подход через JSON незаметно теряет функции, undefined и специальные типы, искажает даты и падает на циклических ссылках — structuredClone справляется со всем этим корректно.

Замечание о сборке мусора

Как только вы начинаете копировать ссылки, стоит помнить, как JavaScript освобождает память. Объект остаётся в памяти только пока он достижим — пока на него ссылается какая-либо переменная, свойство или элемент массива. Когда последняя ссылка на object исчезает, он становится недостижимым и подлежит сборке мусора. Вам никогда не нужно освобождать объекты вручную — движок делает это автоматически.

Реальный пример: копирование состояния перед изменением

Это не абстрактная теория. В современном UI-коде (React, Redux и им подобных) стандартное правило звучит так: «никогда не изменяйте состояние напрямую — создавайте новую копию с применённым изменением». Поверхностное копирование через spread — повседневный инструмент для этого:

javascript— editable

Обратите внимание, что мы используем spread как для объекта верхнего уровня, так и для вложенного array cart. Поскольку spread поверхностный, нужно явно копировать каждый вложенный уровень, который вы планируете изменить, — иначе вы снова попадаете в ловушку разделённых ссылок, описанную выше. Когда данные глубоко вложены и нужна полная безопасная копия, используйте structuredClone.

Резюме

  • Примитивы копируются по значению — копии полностью независимы.
  • Объекты хранятся и копируются по ссылке — копирование переменной копирует указатель, поэтому обе переменные разделяют один object.
  • === сравнивает объекты по ссылке: только один и тот же object равен самому себе.
  • const фиксирует привязку, а не содержимое — объекты с const всё равно можно изменять.
  • Object.assign({}, obj) и {...obj} создают поверхностные копии, разделяющие вложенные объекты.
  • structuredClone(obj) создаёт глубокую копию, обрабатывает циклические ссылки и многие встроенные типы, но не может клонировать функции или DOM-узлы и убирает ключи-symbol, геттеры и прототипы.
  • Предпочитайте structuredClone деструктивному трюку JSON.parse(JSON.stringify(...)).

Проверьте свои знания

Практика
После let a = {}; let b = a; b.x = 1; — чему равно a.x?
После let a = {}; let b = a; b.x = 1; — чему равно a.x?
Практика
Дано: let c = { n: 1 }; let d = { n: 1 }; — что вернёт c === d?
Дано: let c = { n: 1 }; let d = { n: 1 }; — что вернёт c === d?
Практика
Какое утверждение о копировании объектов в JavaScript верно?
Какое утверждение о копировании объектов в JavaScript верно?
Was this page helpful?