Поверхностное и глубокое копирование

При копировании объектов и массивов в JavaScript, данные копируются только на один уровень вглубь.

Время чтения: меньше 5 мин

Кратко

Секция статьи "Кратко"

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

Можно написать функцию глубокого копирования вручную. Скорее всего ваша функция будет рекурсивной, и она будет работать только для конкретных данных — написать универсальную функцию не так-то просто.

Можно воспользоваться готовой библиотекой. Например, функцию глубокого копирования содержит популярная библиотека утилит 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

        
        
          
        
      
Результат глубокого копирования массива

У этого метода есть ограничение — копируемые данные должны быть сериализуемыми. Если объект содержит методы или массив содержит функции, то копирование с помощью JSON-преобразования не сработает:

        
          
          const fns = [  function() { console.log('aaa') },  function() { console.log('bbb') },]const copyFns = JSON.parse(JSON.stringify(fns))console.log(copyFns)// [null, null]
          const fns = [
  function() { console.log('aaa') },
  function() { console.log('bbb') },
]
const copyFns = JSON.parse(JSON.stringify(fns))

console.log(copyFns)
// [null, null]