Поверхностная и глубокая копия

Запись { ...user } создаёт новый объект и переносит в него значения свойств. Пока значениями были строки и числа, копию можно было менять независимо от оригинала.

Но значением свойства бывает и другой объект:

nested.jsjs
const user = {  name: 'Ира',  settings: {    theme: 'dark',  },};const copy = { ...user };console.log(user === copy);
Консоль
false

Объектов действительно два. Но изменим вложенный объект:

nested-change.jsjs
copy.settings.theme = 'light';console.log(user.settings.theme);
Консоль
light

Почему снова изменился оригинал?

Spread копирует только один уровень

nested-user.jsjs
const user = {  name: 'Ира',  settings: {    theme: 'dark',  },};

Посмотрим, какие значения лежат в свойствах user:

  • name → 'Ира' – строка
  • settings → ссылка на объект { theme: 'dark' }

Когда spread переносит свойства, строка 'Ира' копируется как обычное значение, а в settings копируется ссылка на уже существующий вложенный объект. Получается такая структура:

user и copy – разные объекты. Их settings – один общий объект.

Проследим это по шагам:

Новый объект сверху, старая ссылка внутри
const user = {
name: 'Ира',
settings: {
theme: 'dark',
},
};
user ведёт к объекту, а его свойство settings – к вложенному объекту.
const copy = { ...user };
copy.settings.theme = 'light';
console.log(user.settings.theme);

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

Такая копия называется поверхностной: создаётся новый внешний объект, но вложенные объекты автоматически не копируются. Если свойство содержит ссылку на объект, в копию попадёт та же ссылка.

Поэтому user !== copy, но user.settings === copy.settings:

shallow-check.jsjs
const user = {  name: 'Ира',  settings: {    theme: 'dark',  },};const copy = { ...user };console.log(user === copy);console.log(user.settings === copy.settings);
Консоль
false
true

Как сделать вложенный объект независимым

Если settings тоже нужно менять независимо, для него надо создать новый объект:

copy-settings.jsjs
const user = {  name: 'Ира',  settings: {    theme: 'dark',  },};const copy = {  ...user,  settings: {    ...user.settings,  },};

Здесь создаются уже два новых объекта: внешний { ... } создаёт новый copy, а внутренний { ...user.settings } – отдельный объект settings.

У каждого верхнего объекта свой settings.

Теперь copy.settings больше не связан с user.settings. Поэтому изменение copy.settings.theme не затрагивает исходный объект:

copy-settings-check.jsjs
const user = {  name: 'Ира',  settings: {    theme: 'dark',  },};const copy = {  ...user,  settings: {    ...user.settings,  },};copy.settings.theme = 'light';console.log(user.settings.theme);console.log(copy.settings.theme);console.log(user.settings === copy.settings);
Консоль
dark
light
false

А если вложенность ещё глубже?

Допустим, внутри settings лежит ещё один объект:

deeper.jsjs
const user = {  name: 'Ира',  settings: {    theme: {      mode: 'dark',    },  },};const copy = {  ...user,  settings: {    ...user.settings,  },};

Такая копия создаёт новые объекты только для верхнего уровня и для settings, а theme снова остаётся общим. Чтобы сделать независимым и его, пришлось бы продолжить:

deeper-copy.jsjs
const copy = {  ...user,  settings: {    ...user.settings,    theme: {      ...user.settings.theme,    },  },};

Чем глубже структура, тем неудобнее делать это вручную.

Поверхностная и глубокая копия

Поверхностная копия создаёт новый внешний объект, но не копирует вложенные объекты. Поэтому часть структуры может оставаться общей с оригиналом.

Глубокая копия создаёт новые объекты на всех уровнях вложенности. В результате изменение вложенной части копии не изменяет соответствующую часть оригинала.

В сравнениях это выглядит так:

shallow-vs-deep.jsjs
// поверхностнаяuser !== copy;user.settings === copy.settings;// глубокаяuser !== copy;user.settings !== copy.settings;

structuredClone

Для многих обычных структур современный JavaScript умеет создать глубокую копию сам:

structured-clone.jsjs
const user = {  name: 'Ира',  settings: {    theme: 'dark',  },};const copy = structuredClone(user);copy.settings.theme = 'light';console.log(user.settings.theme);console.log(copy.settings.theme);
Консоль
dark
light

structuredClone() умеет глубоко копировать многие обычные структуры данных, но не любые: если в свойстве лежит функция, вызов завершится ошибкой DataCloneError.

Способы копирования

Выбор способа сводится к двум вопросам: есть ли в объекте вложенные объекты и должны ли они стать независимыми от оригинала. Если вложенных объектов нет или они могут остаться общими, достаточно поверхностной копии. Если они должны стать независимыми, нужна глубокая копия.

Если нужна поверхностная копия

Основной способ, который вы будете использовать:

const copy = { ...user };

Также существуют Object.assign({}, user) и ручной перенос свойств циклом for...in – их полезно было увидеть, чтобы понять принцип.

Если нужна глубокая копия

Для данных, которые поддерживает structuredClone:

const copy = structuredClone(user);

Похожую функцию cloneDeep предлагает библиотека Lodash.

А если нужно сделать независимым только конкретный вложенный объект, для него пишут отдельный литерал:

copy-one-nested.jsjs
const copy = {  ...user,  settings: {    ...user.settings,  },};

// теперь вы знаете

  1. { ...user } создаёт новый внешний объект, но значения его свойств копируются как есть. Если значением является ссылка на другой объект, эта ссылка остаётся общей
  2. Поэтому поверхностная копия может быть независима сверху, но иметь общие вложенные объекты: user !== copy, но user.settings === copy.settings
  3. Если независимым нужен конкретный вложенный объект, его тоже можно скопировать через spread: settings: { ...user.settings }
  4. Копию, в которой вся вложенная структура независима от оригинала, называют глубокой
  5. Для глубокой копии многих обычных данных можно использовать structuredClone(user)

Проверьте себя

Что появится в консоли?

const player = {
name: 'Ира',
stats: {
level: 3,
},
};
const copy = { ...player };
copy.name = 'Макс';
copy.stats.level = 10;
console.log(player.name);
console.log(player.stats.level);

У объекта point нет вложенных объектов. Верно ли, что a и b одинаково независимы от point – присваивание любому их свойству не меняет point?

const point = {
x: 1,
y: 2,
};
const a = { ...point };
const b = structuredClone(point);

Что появится в консоли?

const note = {
title: 'Покупки',
style: {
color: 'green',
},
};
const copy = {
style: { ...note.style },
...note,
};
copy.style.color = 'red';
console.log(note.style.color);
console.log(copy.style === note.style);

Сколько новых объектов создаёт последняя строка?

const order = {
id: 7,
customer: {
name: 'Ира',
address: {
city: 'Казань',
},
},
paid: true,
};
const copy = structuredClone(order);

После какой записи строка copy.settings.theme = 'light'; не изменит user.settings.theme?

const user = {
name: 'Ира',
settings: {
theme: 'dark',
},
};