Кратко
СкопированоПри копировании объектов или массивов JavaScript копирует данные только на один уровень вглубь. Этот тип копирования называется поверхностным (shallow).
Если необходимо полностью скопировать сложную структуру данных, например, массив с объектами, то нужно делать глубокое (deep) или полное копирование данных. JavaScript не содержит функций для глубокого копирования, лучший вариант сделать глубокую копию — сериализовать структуру в JSON и тут же распарсить.
Как понять
СкопированоПроблема поверхностного копирования
СкопированоПоверхностное копирование работает быстро и в большинстве случаев его достаточно. Проблемы появляются, когда приходится копировать вложенные структуры:
const itemsInCart = [ { product: 'Носки', quantity: 3 }, { product: 'Штаны', quantity: 1 }, { product: 'Кепка', quantity: 1 },]const clonedCart = [...itemsInCart]
const itemsInCart = [ { product: 'Носки', quantity: 3 }, { product: 'Штаны', quantity: 1 }, { product: 'Кепка', quantity: 1 }, ] const clonedCart = [...itemsInCart]
Если изменять элементы этой структуры после копирования, то эти изменения будут также видны в исходной структуре:
clonedCart[1].quantity = 5console.log(clonedCart)// [// { product: 'Носки', quantity: 3 },// { product: 'Штаны', quantity: 5 },// { product: 'Кепка', quantity: 1 },// ]console.log(itemsInCart)// [// { product: 'Носки', quantity: 3 },// { product: 'Штаны', quantity: 5 },// { product: 'Кепка', quantity: 1 },// ]
clonedCart[1].quantity = 5 console.log(clonedCart) // [ // { product: 'Носки', quantity: 3 }, // { product: 'Штаны', quantity: 5 }, // { product: 'Кепка', quantity: 1 }, // ] console.log(itemsInCart) // [ // { product: 'Носки', quantity: 3 }, // { product: 'Штаны', quantity: 5 }, // { product: 'Кепка', quantity: 1 }, // ]
Непримитивные типы данных, такие как массивы и объекты, хранятся по ссылке. Так как копирование происходит только на один уровень вглубь, то при копировании массива происходит копирование ссылок на старые объекты в новый массив.
В итоге получается картина, когда разные массивы ссылаются на одни и те же объекты в памяти:
console.log(itemsInCart[1] === clonedCart[1])// true
console.log(itemsInCart[1] === clonedCart[1]) // true
Как получить глубокую копию
СкопированоВ JavaScript есть функция structuredClone() для глубокого копирования массивов или объектов. На странице с документацией можно проверить доступность этой функции для разных версий: например, она доступна в NodeJS, начиная с версии 17.0.0.
const deep = structuredClone(itemsInCart)console.log(itemsInCart[1] === deep[1])// false
const deep = structuredClone(itemsInCart) console.log(itemsInCart[1] === deep[1]) // false
Существуют и другие способы сделать глубокое копирование. Можно написать функцию глубокого копирования вручную. Скорее всего ваша функция будет рекурсивной, и она будет работать только для конкретных данных — написать универсальную функцию не так-то просто.
Можно воспользоваться готовой библиотекой. Например, функцию глубокого копирования содержит популярная библиотека утилит lodash. Функция гарантировано работает в подавляющем большинстве случаев, потому что используется в десятках тысяч проектов каждый день. Исходный код библиотеки открыт, можно изучить исходный код функции глубокого копирования.
import cloneDeep from 'lodash.clonedeep'const deep = cloneDeep(itemsInCart)console.log(itemsInCart[1] === deep[1])// false
import cloneDeep from 'lodash.clonedeep' const deep = cloneDeep(itemsInCart) console.log(itemsInCart[1] === deep[1]) // false
Ещё один способ глубокого копирования звучит достаточно глупо — нужно сериализовать копируемый объект в JSON и тут же распарсить его. В результате появится полная копия объекта:
const deep = JSON.parse(JSON.stringify(itemsInCart))console.log(itemsInCart[1] === deep[1])// false
const deep = JSON.parse(JSON.stringify(itemsInCart)) console.log(itemsInCart[1] === deep[1]) // false
У этого метода есть ограничение — копируемые данные должны быть сериализуемы.
Вот примеры несериализуемых данных: примитив undefined, функция, symbol - при вызове JSON.stringify получаем undefined
Массивы и объекты - сериализуемы. Что будет если у них в качестве ключа или значения будут несериализуемые данные?
- для массивов: такие значения будут превращены в null;
- для объектов: такие значения будут опущены, а если symbol является ключом объекта, то он будет проигнорирован, даже при использовании функции replacer.
const arr = [ undefined, function() { console.log('aaa') }, Symbol("foo"),]const copyArr = JSON.parse(JSON.stringify(arr))console.log(copyArr)// [null, null, null]const obj = { a: undefined, method: () => {}, [Symbol("foo")]: "foo",}const copyObj = JSON.parse(JSON.stringify(obj), function(k, v) { if (typeof k === 'symbol') { return 'символ'; } return v;})console.log(copyObj)// {}
const arr = [ undefined, function() { console.log('aaa') }, Symbol("foo"), ] const copyArr = JSON.parse(JSON.stringify(arr)) console.log(copyArr) // [null, null, null] const obj = { a: undefined, method: () => {}, [Symbol("foo")]: "foo", } const copyObj = JSON.parse(JSON.stringify(obj), function(k, v) { if (typeof k === 'symbol') { return 'символ'; } return v; }) console.log(copyObj) // {}