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] клонирует объект.