JavaScript

Поскольку объекты в #JavaScript являются ссылочными значениями, их нельзя просто скопировать с помощью =. Но не беспокойтесь, существует 3 способа клонирования объекта 👍.

const food = { beef: '🥩', bacon: '🥓' }

// "Spread"
{ ...food }

// "Object.assign"
Object.assign({}, food)

// "JSON"
JSON.parse(JSON.stringify(food))

// RESULT:
// { beef: '🥩', bacon: '🥓' }

Объекты — это ссылочные типы

Почему нельзя использовать =? Посмотрим, что может произойти:

const obj = {one: 1, two: 2};

const obj2 = obj;

console.log(
  obj,  // {one: 1, two: 2};
  obj2  // {one: 1, two: 2};
)

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

const obj2.three = 3;

console.log(obj2);
// {one: 1, two: 2, three: 3}; <-- ✅

console.log(obj);
// {one: 1, two: 2, three: 3}; <-- 😱

obj2 был изменен, однако изменения коснулись и obj. Причина заключается в том, что объекты являются ссылочными типами. Поэтому при использовании =, указатель копируется в область занимаемой памяти. Ссылочные типы не содержат значений, они являются указателем на значение в памяти.

Использование Spread

С помощью spread можно клонировать объект. Обратите внимание, что копия будет неглубокой. На момент публикации этого руководства оператор spread для клонирования объектов находился на стадии 4, соответственно официально он не указан в спецификациях. Поэтому для того, чтобы его использовать, нужно выполнить компиляцию с Babel (или чем-то подобным).

const food = { beef: '🥩', bacon: '🥓' };

const cloneFood = { ...food };

console.log(cloneFood); 
// { beef: '🥩', bacon: '🥓' }

Использование Object.assign

Object.assign, выпущенный официально, также создает неглубокую копию объекта.

const food = { beef: '🥩', bacon: '🥓' };

const cloneFood = Object.assign({}, food);

console.log(cloneFood);
// { beef: '🥩', bacon: '🥓' }

Использование JSON

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

const food = { beef: '🥩', bacon: '🥓' };

const cloneFood = JSON.parse(JSON.stringify(food))

console.log(cloneFood); 
// { beef: '🥩', bacon: '🥓' }

Lodash DeepClone или JSON?

  • JSON.stringify/parse работает только с литералом Number, String и Object без функции или свойства Symbol.
  • deepClone работает со всеми типами, а функция и символ копируются по ссылке.

Пример:

const lodashClonedeep = require("lodash.clonedeep");

const arrOfFunction = [() => 2, {
    test: () => 3,
}, Symbol('4')];

// копия deepClone по ссылочной функции и символу
console.log(lodashClonedeep(arrOfFunction));
// JSON заменяет функцию на ноль и функцию в объекте на undefined
console.log(JSON.parse(JSON.stringify(arrOfFunction)));

// функция и символ копируются по ссылке в deepClone
console.log(lodashClonedeep(arrOfFunction)[0] === lodashClonedeep(arrOfFunction)[0]);
console.log(lodashClonedeep(arrOfFunction)[2] === lodashClonedeep(arrOfFunction)[2]);

Глубокое или неглубокое клонирование?

При использовании spread для копирования объекта создается неглубокая копия. Если массив является вложенным или многомерным, этот способ не будет работать. Рассмотрим пример:

const nestedObject = {
  country: '🇨🇦',
  {
    city: 'vancouver'
  }
};

const shallowClone = { ...nestedObject };

// Изменение клонированного объекта
clonedNestedObject.country = '🇹🇼'
clonedNestedObject.country.city = 'taipei';

Таким образом, клонированный объект был изменен с добавлением city. В результате получаем:

console.log(shallowClone);
// {country: '🇹🇼', {city: 'taipei'}} <-- ✅

console.log(nestedObject);
// {country: '🇨🇦', {city: 'taipei'}} <-- 😱

Неглубокая копия предполагает копирование первого уровня и ссылается на более глубокие уровни.

Глубокая копия

Возьмем тот же пример, но применим глубокую копию с использованием JSON:

const deepClone = JSON.parse(JSON.stringify(nestedObject));

console.log(deepClone);
// {country: '🇹🇼', {city: 'taipei'}} <-- ✅

console.log(nestedObject);
// {country: '🇨🇦', {city: 'vancouver'}} <-- ✅

Глубокая копия является копией для вложенных объектов. Однако иногда достаточно использования неглубокой копии.

Производительность

К сожалению, на данный момент нельзя написать тестирование для spread, поскольку официально он не указан в спецификации. Однако результат показывает, что Object.assign намного быстрее, чем JSON. Тест на производительность можно найти здесь.

Object.assign и Spread

Стоит отметить, что Object.assign — это функция, которая модифицирует и возвращает целевой объект. В данном примере при использовании:

const cloneFood = Object.assign({}, food)

{} — это модифицируемый объект. В этой точке на целевой объект не ссылаются никакие переменные, но поскольку Object.assign возвращает целевой объект, то можно сохранить полученный присвоенный объект в переменную cloneFood. Данный пример можно изменить следующим образом:

const food = { beef: '🌽', bacon: '🥓' };

Object.assign(food, { beef: '🥩' });

console.log(food);
// { beef: '🥩', bacon: '🥓' }

Очевидно, что значение beef в объекте food неверно, поэтому нужно назначить правильное значение beef с помощью Object.assign. На самом деле мы не используем возвращаемое значение функции, а изменяем целевой объект, на который ссылаемся с помощью константы food.

С другой стороны, Spread — это оператор, который копирует свойства одного объекта в новый объект. При репликации приведенного выше примера с помощью spread для изменения переменной food...

const food = { beef: '🌽', bacon: '🥓' };

food = {
  ...food,
  beef: '🥩',
}
// TypeError: invalid assignment to const `food'

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

const food = { beef: '🌽', bacon: '🥓' };

const newFood = {
  ...food,
  beef: '🥩',
}

console.log(newFood);
// { beef: '🥩', bacon: '🥓' }

либо объявить food с let или var, что позволит присвоить новый объект:

let food = { beef: '🌽', bacon: '🥓' };

food = {
  ...food,
  beef: '🥩',
}

console.log(food);
// { beef: '🥩', bacon: '🥓' }

Спасибо за внимание!

Читайте также:


Перевод статьи Samantha Ming: 3 Ways to Clone Objects in JavaScript