Копирование объектов

Присваивание не создаёт копию объекта:

same-ref.jsjs
const user = {  name: 'Ира',  age: 20,};const copy = user;

После этого user и copy ведут к одному объекту:

Одно присваивание – две ссылки на один объект.

Поэтому изменение через copy видно и через user:

same-ref-change.jsjs
copy.name = 'Макс';console.log(user.name);
Консоль
Макс

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

Копирование вручную

Самый прямой способ – создать новый литерал и перенести свойства вручную:

manual.jsjs
const user = {  name: 'Ира',  age: 20,};const copy = {  name: user.name,  age: user.age,};copy.name = 'Макс';console.log(user.name);console.log(copy.name);
Консоль
Ира
Макс

Литерал { ... } выполнился ещё раз, поэтому объектов теперь два:

Изменение copy.name не касается user.name.

Теперь user и copy – два разных объекта. У каждого свои свойства, поэтому изменение copy.name меняет только copy и никак не влияет на user.

Почему это работает

Сравните две записи:

копируется ссылка
const copy = user;
создаётся новый объект
const copy = {  name: user.name,  age: user.age,};

В первом случае нового объекта нет вообще – копируется ссылка. Во втором выполняется новый литерал { ... }: JavaScript создаёт новый объект и записывает в него значения свойств user. Именно новый объект делает копию независимой от оригинала.

Но вручную копировать неудобно

Для двух свойств такой код ещё терпим:

two-props.jsjs
const copy = {  name: user.name,  age: user.age,};

А если свойств десять?

profile.jsjs
const profile = {  name: 'Ира',  age: 20,  city: 'Москва',  role: 'student',  active: true,};

Переписывать каждое свойство вручную неудобно, тем более что состав объекта со временем меняется. Нужен способ пройти по свойствам автоматически.

Копирование перебором

Цикл for...in позволяет перебрать свойства объекта. На каждой итерации key получает имя очередного свойства.

for-in.jsjs
const user = {  name: 'Ира',  age: 20,  city: 'Москва',};const copy = {};for (const key in user) {  copy[key] = user[key];}console.log(copy);
Консоль
{ name: 'Ира', age: 20, city: 'Москва' }

Разберём центральную строку:

copy-line.jsjs
copy[key] = user[key];

Сначала key равен 'name', и строка по смыслу превращается в:

key-name.jsjs
copy['name'] = user['name'];

Потом key равен 'age':

key-age.jsjs
copy['age'] = user['age'];

Так пустой объект по одному получает те же свойства.

Свойства копируются по одному
const user = {
name: 'Ира',
age: 20,
city: 'Москва',
};
const copy = {};
Создали новый пустой объект copy. Свойств в нём пока нет.
for (const key in user) {
copy[key] = user[key];
}
console.log(copy);
Консоль
Нет вывода в консоль.

Object.assign: копирование свойств

Копировать свойства вручную через цикл можно, но такая задача встречается часто. Поэтому в JavaScript есть готовый метод Object.assign. Он берёт свойства одного или нескольких объектов и записывает их в другой объект:

assign-arguments.jsjs
Object.assign(target, source1, source2);

Первый аргумент – объект, который будет изменён: именно в него попадут свойства. Все следующие аргументы – объекты, из которых свойства берутся.

Например, добавим настройки звука в уже существующий объект:

assign-into-settings.jsjs
const settings = {  theme: 'dark',};const sound = {  volume: 50,};Object.assign(settings, sound);console.log(settings);
Консоль
{ theme: 'dark', volume: 50 }

После вызова settings содержит и своё старое свойство theme, и новое свойство volume, взятое из sound.

Важно: Object.assign здесь не создавал второй объект. Он изменил уже существующий settings.

Object.assign возвращает изменённый объект

У Object.assign есть ещё одна особенность: результатом вызова становится тот же объект, который стоял первым аргументом. Вызов Object.assign(settings, { volume: 70 }) сначала добавляет volume в settings, а затем возвращает сам settings:

assign-returns.jsjs
const settings = {  theme: 'dark',};const same = settings;const result = Object.assign(settings, { volume: 70 });console.log(same.volume);console.log(result === settings);
Консоль
70
true

same, result и settings ведут к одному объекту. Никакой копии в этом примере не создавалось.

Как с помощью Object.assign получить копию

Вызов Object.assign(settings, sound) нужен прежде всего для изменения существующего объекта. Но если первым аргументом передать новый пустой объект {}, свойства запишутся уже в него, а исходный объект останется нетронутым.

Возьмём объект user и вместо существующего объекта первым аргументом передадим новый пустой литерал:

object-assign.jsjs
const user = {  name: 'Ира',  age: 20,};const copy = Object.assign({}, user);

Object.assign записывает свойства user в новый объект {} и возвращает этот новый объект – поэтому его и получает переменная copy.

По смыслу происходит то же, что мы только что делали циклом: свойства user одно за другим записываются в новый объект. Object.assign просто выполняет эту работу за нас.

Проверим:

object-assign-check.jsjs
copy.name = 'Макс';console.log(user.name);console.log(copy.name);console.log(user === copy);
Консоль
Ира
Макс
false

Теперь user и copy – два разных объекта. Изменение свойства одного не меняет такое же свойство другого.

Несколько объектов за один вызов

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

merge.jsjs
const base = {  name: 'Ира',  age: 20,};const extra = {  city: 'Москва',};const user = Object.assign({}, base, extra);console.log(user);
Консоль
{ name: 'Ира', age: 20, city: 'Москва' }

Сначала в новый {} попадут свойства base, затем – свойства extra. Так удобно собирать один объект из нескольких частей.

Если свойства совпадают

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

merge-override.jsjs
const user = Object.assign(  {},  { name: 'Ира', age: 20 },  { age: 21 });console.log(user);
Консоль
{ name: 'Ира', age: 21 }

Сначала age получил значение 20, а следующий объект снова записал свойство age – уже со значением 21. Остаётся значение, записанное позже: 20 → 21.

Object.assign умеет и изменять существующие объекты, и создавать копии через пустой {}. Но для простого копирования запись выглядит громоздко:

const copy = Object.assign({}, user);

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

Копирование через spread

Для этого используется синтаксис ... – :

spread.jsjs
const user = {  name: 'Ира',  age: 20,};const copy = {  ...user,};

Фигурные скобки создают новый объект, а ...user переносит внутрь него свойства user.

Запись можно читать почти буквально: «создай новый объект и скопируй в него свойства user».

Обе записи создают новый объект с теми же свойствами.

Объекты разные, и изменение копии не меняет оригинал:

spread-check.jsjs
console.log(user === copy);copy.age = 21;console.log(user.age);console.log(copy.age);
Консоль
false
20
21

Что именно делает ...user

Результат записи { ...user } можно представить так:

spread
const copy = {  ...user,};
то же вручную
const copy = {  name: user.name,  age: user.age,};

Spread не клонирует объект каким-то особым образом – он переносит его свойства в новый литерал. Новый объект появляется благодаря { }, а ...user определяет, какие свойства в него положить.

Копия с изменениями

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

updated.jsjs
const user = {  name: 'Ира',  age: 20,  city: 'Москва',};const updated = {  ...user,  age: 21,};console.log(updated);console.log(user.age);
Консоль
{ name: 'Ира', age: 21, city: 'Москва' }
20

В updated сначала попадают все свойства user: name, age и city. Затем строка age: 21 записывает age ещё раз – остаётся последнее значение. Исходный объект при этом не изменился.

Общий вид приёма: сначала ...user, затем свойства, которые нужно добавить или заменить.

Порядок имеет значение

source-user.jsjs
const user = {  name: 'Ира',  age: 20,  city: 'Москва',};

Две записи ниже дают разный результат:

spread-then-age.jsjs
const a = {  ...user,  age: 100,};console.log(a.age);
Консоль
100
age-then-spread.jsjs
const b = {  age: 100,  ...user,};console.log(b.age);
Консоль
20

В a свойство age записано последним, поэтому там 100. В b сначала записывается 100, но затем ...user снова переносит исходный age, и остаётся 20. Правило простое: если свойство записали несколько раз, остаётся последнее значение.

Одна задача – три способа

Мы разобрали три способа создать новый объект на основе уже существующего:

three-ways.jsjs
// вручнуюconst copy1 = {};for (const key in user) {  copy1[key] = user[key];}// Object.assignconst copy2 = Object.assign({}, user);// spreadconst copy3 = { ...user };

Во всех наших простых примерах результат один: появляется новый объект, в который копируются свойства user. Различается только способ записи:

  • цикл делает копирование вручную – мы сами берём свойства одно за другим;
  • Object.assign делает эту работу готовым методом;
  • spread ... позволяет записать ту же задачу короче и нагляднее прямо внутри нового объекта.

Поэтому дальше для обычного копирования мы чаще будем использовать spread:

const copy = { ...user };

Его удобно не только копировать, но и сразу дополнять или изменять:

updated-user.jsjs
const updatedUser = {  ...user,  age: 21,  city: 'Москва',};

При этом понимать Object.assign всё равно нужно: в уже написанном JavaScript-коде он встречается часто.

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

  1. const copy = user копирует ссылку, а не объект: новый объект появляется там, где выполняется литерал { … }
  2. for...in перебирает имена свойств, а copy[key] = user[key] записывает соответствующие значения в новый объект
  3. Object.assign записывает свойства в объект из первого аргумента и возвращает этот объект; Object.assign({}, user) даёт новый объект с теми же свойствами
  4. Spread { ...user } делает то же короче, а { ...user, age: 21 } даёт копию с изменением
  5. Если свойство записано в литерале несколько раз, остаётся последнее значение – поэтому порядок важен

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

Верно ли, что после строки book.pages = 500; и same.pages, и copy.pages равны 500?

const book = {
title: 'Дюна',
pages: 412,
};
const same = book;
const copy = {
title: book.title,
pages: book.pages,
};
book.pages = 500;

Верно ли, что после вызова в объекте profile появилось свойство city?

const profile = { name: 'Ира' };
const address = { city: 'Казань' };
Object.assign(address, profile);

Что напечатает код?

const saved = { size: 16 };
const defaults = { theme: 'light', size: 14 };
const settings = Object.assign({}, saved, defaults);
console.log(settings.size);

Какая запись создаёт такой же объект, как { ...product }: новый, с теми же свойствами и значениями?

const product = {
title: 'Лампа',
price: 1200,
};

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

const order = {
id: 7,
status: 'new',
total: 500,
};
const paid = {
status: 'paid',
...order,
total: 450,
};
console.log(paid.status);
console.log(paid.total);

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

const user = { name: 'Ира' };
const a = { ...user };
const b = Object.assign({}, user);
const c = Object.assign(user, {});
a.name = 'Макс';
b.name = 'Лена';
c.name = 'Ян';
console.log(user.name);