3 способа клонирования объектов в 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 также был затронут. Это потому, что объекты являются ссылочными типами. Поэтому, когда вы используете =, он копирует указатель на место в памяти, которое он занимает. Ссылочные типы не хранят значения, они являются указателем на значение.

1. Использование спред (...spread)

Использование spread приведет к клонированию объекта. Обратите внимание, что это будет поверхностная копия.

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

const cloneFood = { ...food };

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

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

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

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

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

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

Обратите внимание на пустой {} в качестве первого аргумента, это позволит вам не мутировать исходный объект 👍

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

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

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

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

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

Lodash DeepClone vs JSON

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

Приведу пример:

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

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

// deepClone copy by refence function and Symbol
console.log(lodashClonedeep(arrOfFunction));
// JSON replace function with null and function in object with undefined
console.log(JSON.parse(JSON.stringify(arrOfFunction)));

// function and symbol are copied by reference in deepClone
console.log(
  lodashClonedeep(arrOfFunction)[0] === lodashClonedeep(arrOfFunction)[0],
);
console.log(
  lodashClonedeep(arrOfFunction)[2] === lodashClonedeep(arrOfFunction)[2],
);

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

Поверхностное vs Глубокое клонирование

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

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

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

Давайте клонируем наш объект, используя spread:

const shallowClone = { ...nestedObject };

// Changed our cloned object
shallowClone.flag = '🇹🇼';
shallowClone.country.city = 'taipei';

Итак, мы изменили наш клонированный объект, изменив город. Давайте посмотрим на результат.

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'}} <-- ✅

Как вы можете видеть, глубокая копия является истинной копией для вложенных объектов. Часто поверхностная копия достаточно хороша, и глубокая копия не нужна. Это как гвоздодер против молотка. В большинстве случаев молоток вполне подходит. Использование гвоздодера для мелких художественных работ часто является излишеством, а молоток - в самый раз. Все дело в использовании правильного инструмента для правильной работы 🤓

Object.assign vs 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 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: '🥓' }

Глубокое клонирование с использованием внешних библиотек

Лично я использую jQuery с функцией $.extend()

underscore.js ~~ _.clone()

В библиотеке Lodash, метод cloneDeep.

Другие способы использования в JavaScript

Object.fromEntries(Object.entries(food)) [shallow] клонирует объект.

Написать комментарий