Проблема, с которой рано или поздно сталкивается каждый разработчик - копирование данных. И если с примитивными типами данных все более-менее просто, работа с объектами может вызвать головную боль.
Распространенный пример проблемы: у вас есть объект, хранящийся в переменной с именем 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 в том, что он был создан именно для целей клонирования и копирования объекта.
Пример использования метода:
// Создадим объект с циклической ссылкой
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. Также, если ваш объект содержит функции, они будут удалены из результата.
Комментарии (0)