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

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

let user = { name: "Иван", age: 30 };

Допустим, мы хотим изменить поле name, но при этом сохранить прежние данные объекта. 

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

Правильней будет скопировать объект в новую переменную и там уже обновить данные. Подобное выражение let user1 = user является частой ошибкой разработчиков, т.к. оно всего лишь создает ссылку на исходный объект, и если мы сделаем изменения в user1, то они применятся и к user. А нам нужен именно независимый отдельный клон.

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

Использование спред-оператора или метода Object.assign()

Один из самых простых способов скопировать объект в JavaScript - использовать метод Object.assign, который копирует все свойства из одного или нескольких исходных объектов в один целевой объект.

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

let user = { name: "Иван", age: 30 };
let clonedUser = Object.assign({}, user);
console.log(clonedUser);  // => { name: "Иван", age: 30 }

Более короткий способ сделать то же самое - использовать спред-синтаксис, который позволяет «расширять» выражения объектов. Одним из следствий этого является то, что мы можем расширить новый объект парами ключ/значение из исходного объекта, фактически создав клон.

let user = { name: "Иван", age: 30 };
let newUser = { ...user };  // передаем все данные объекта user в объект newUser
console.log(newUser);  // => { name: "Иван", age: 30 }

По сути, ...user заменяется всеми ключами и значениями из user.

Оба метода функционально идентичны, единственное отличие состоит в том, что Object.assign принимает свойства целевого объекта, тогда как использование спред-оператора создает новый объект. Это означает, что Object.assign также подтягивает любые сеттеры объекта (эта информация для большинства не обязательна, однако как замечание - интересна).

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

const user = {
  id: "USER123",
  name: {
    first: "Иван",
    last: "Иванов",
  },
};
const newUser = { ...user };
newUser.id = "newUSER123";
console.log(user);  // => { id: "USER123", name: { first: "Иван", last: "Иванов" } }
console.log(newUser);  // => { id: "newUSER123", name: { first: "Иван", last: "Иванов" } }

// Изменим объект внутри поверхностной копии
newUser.name.first = "Михаил";
console.log(user);  // => { id: "USER123", name: { first: "Михаил", last: "Иванов" } }
console.log(newUser);  // => { id: "newUSER123", name: { first: "Михаил", last: "Иванов" } }

Поверхностная копия объекта означает, что при изменении непримитивных значений (объект name в примере) будет изменен клон, но вместе с ним и исходный объект.

Это связано с тем, что любые значения, хранящиеся как объект, являются ссылочными значениями, и когда вы копируете объект с другими объектами внутри, вы копируете объект со ссылками на эти другие объекты.

Преобразование объекта в строку JSON и обратно

Этот метод использует JSON (JavaScript Object Notation), который может хранить информацию о парах ключ/значение и поддерживает большинство основных типов данных.

Метод JSON.stringify позволяет нам передать объект в первом аргументе для преобразования в строковый формат. Как только мы преобразовали наш объект в строку, мы можем взять эту строку и преобразовать ее обратно в исходный объект, используя метод JSON.parse.

let name = { id: 1, name: "Иван" };
let clonedName = JSON.parse(JSON.stringify(name));
console.log(clonedName);  // => { id: 1, name: "Иван" }

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

Однако, поскольку JSON поддерживает только ограниченное подмножество типов данных в JavaScript (такие, как логические значения, строки, массивы и т. д.), такие вещи, как Date или Maps, не будут правильно преобразованы при создании клона, если не предпринять дополнительных шагов для преобразования несовместимых значений в объекте сначала во что-то, что может понять JSON, а затем обратно в желаемый тип данных.

Кроме того, JSON.stringify не сможет преобразовать значение в строку, если значение ссылается на себя. Такое значение называется циклической ссылкой, и попытка преобразовать объект, который ссылается на себя, приведет к бесконечному циклу. Пример подобного поведения:

const circularReference = {};
circularReference.myself = circularReference;
JSON.stringify(circularReference);

Использование глобального метода structuredClone

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

Поддержка браузерами
chrome
Chrome
98
firefox
Firefox
94
internet explorer
IE
 
edge
Edge
98
safari
Safari
15.4
opera
Opera
84

Пример использования метода:

// Создадим объект с циклической ссылкой
let user = { name: "Иван" };
user.self = user;

// Клонируем объект
let clonedUser = structuredClone(user);

console.log(clonedUser !== user);  // => true
console.log(clonedUser.name === "Иван");  // => true
console.log(clonedUser.self === clonedUser);  // => true

В отличие от метода JSON.parse/stringify, данный поддерживает циклические ссылки и сохраняет большинство значений и типов данных.

Однако следует отметить, что structuredClone выдает ошибку при клонировании узлов DOM. Также, если ваш объект содержит функции, они будут удалены из результата.