Кратко
СкопированоПри копировании объекта вложенные свойства (nested properties) не дублируются, а сохраняются в новом объекте как ссылки. Этот тип копирования называется поверхностным (shallow).
Если необходимо полностью скопировать сложную структуру данных, например, массив объектов, то нужно делать глубокое (deep) или полное копирование данных.
Как понять
СкопированоПроблема поверхностного копирования
СкопированоПоверхностное копирование работает быстро и в большинстве случаев его достаточно. Проблемы появляются, когда приходится копировать вложенные структуры:
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

Как получить глубокую копию
СкопированоСуществует несколько способов выполнить глубокое копирование:
- собственная функция копирования;
- глобальная функция
structured;Clone ( ) - преобразование с помощью функций
JSONи. stringify ( ) JSON;. parse ( ) - сторонние библиотечные функции, например
cloneиз библиотекиDeep ( ) Lodash.
У каждого способа есть свои ограничения, потому что не все объекты могут быть полностью клонированы.
Своя функция копирования объектов
СкопированоМожно написать свою функцию глубокого копирования. Скорее всего ваша функция будет рекурсивной, и она будет работать только для конкретных данных — написать универсальную функцию не так-то просто.
Создадим, для примера, функцию копирования простых объектов (plain object) или массивов:
function createCopy(object) { if (object === null || typeof object !== 'object') { return object } const keys = Object.keys(object) const clonedObject = keys.reduce((acc, key) => { acc[key] = createCopy(object[key]) return acc }, Array.isArray(object) ? [] : {}) return clonedObject}
function createCopy(object) {
if (object === null || typeof object !== 'object') {
return object
}
const keys = Object.keys(object)
const clonedObject = keys.reduce((acc, key) => {
acc[key] = createCopy(object[key])
return acc
}, Array.isArray(object) ? [] : {})
return clonedObject
}
Функция create подойдёт для копирования при условии, что исходный объект:
- содержит в качестве значений вложенные массивы, простые объекты или примитивы (кроме
Symbol); - не содержит Symbol-ключи;
- не содержит циклических ссылок;
- не наследует изменений в прототипах.
Глобальная функция structuredClone()
СкопированоВо многих случаях предпочтительным будет применить глобальную функцию structured. Она не описывается спецификацией ECMAScript (и поэтому не является частью языка), но доступна в браузерах благодаря Web API, а также реализована в Node.js и в других средах исполнения кода.
const clonedCart = structuredClone(itemsInCart)console.log(itemsInCart[1] === clonedCart[1])// false
const clonedCart = structuredClone(itemsInCart)
console.log(itemsInCart[1] === clonedCart[1])
// false
Возможности structured:
- поддержка копирования значений множества типов, например: Map, Set, Date, ArrayBuffer, TypedArray, DateView;
- корректная обработка циклических ссылок;
- реализация перемещения ресурсов (transferable objects) от исходного объекта к копии.
Перемещение обеспечивает безопасность доступа к ресурсу. Например, при передаче типизированного массива в сообщении от основного потока к веб-воркеру буфер двоичных данных будет перемещён и доступен только веб-воркеру.
Рассмотрим на примере как structured помогает перемещать ReadableStream-данные при копировании:
// Создадим поток данныхconst stream = new ReadableStream({ start(controller) { controller.enqueue("<header>") controller.enqueue("<main>") controller.enqueue("<footer>") controller.close() }})// Добавим потоком к объектуconst obj = { stream }// Копируем объект с передачей (transfer) потокаconst cloned = structuredClone(obj, { transfer: [obj.stream] })try { // Попытаемся получить данные потока из оригинального объекта const reader1 = obj.stream.getReader() console.log('Читаем поток из оригинала:', await reader1.read())} catch (e) { console.log('Ошибка доступа к потоку:', e.message)}// Ошибка доступа к потоку: Invalid state: ReadableStream is locked// Получим дынные потока из копии объектаconst reader2 = cloned.stream.getReader()console.log('Читаем поток из копии:')while (true) { const { value, done } = await reader2.read() if (done) break console.log(value)}// <header>// <main>// <footer>
// Создадим поток данных
const stream = new ReadableStream({
start(controller) {
controller.enqueue("<header>")
controller.enqueue("<main>")
controller.enqueue("<footer>")
controller.close()
}
})
// Добавим потоком к объекту
const obj = { stream }
// Копируем объект с передачей (transfer) потока
const cloned = structuredClone(obj, { transfer: [obj.stream] })
try {
// Попытаемся получить данные потока из оригинального объекта
const reader1 = obj.stream.getReader()
console.log('Читаем поток из оригинала:', await reader1.read())
} catch (e) {
console.log('Ошибка доступа к потоку:', e.message)
}
// Ошибка доступа к потоку: Invalid state: ReadableStream is locked
// Получим дынные потока из копии объекта
const reader2 = cloned.stream.getReader()
console.log('Читаем поток из копии:')
while (true) {
const { value, done } = await reader2.read()
if (done) break
console.log(value)
}
// <header>
// <main>
// <footer>
Если попытаться выполнить копирование объекта без указания перемещаемого ресурса, получим ошибку:
const obj = { stream }const cloned = structuredClone(obj)// DOMException [DataCloneError]: Object that needs transfer was found in message but not listed in transferList
const obj = { stream }
const cloned = structuredClone(obj)
// DOMException [DataCloneError]: Object that needs transfer was found in message but not listed in transferList
Выполнение structured завершится ошибкой DataClone если копируемый объект содержит:
- DOM-элементы;
Function;Symbol-значения;Weak-объекты;Map Weak-объекты;Set Proxy-объекты.
В копии не сохранятся:
Symbol-ключи;- изменения прототипов копируемого объекта;
- приватные поля экземпляров классов.
Преобразование с помощью функций JSON.stringify() и JSON.parse()
СкопированоЕщё один способ глубокого копирования звучит достаточно глупо — нужно сериализовать копируемый объект в JSON и тут же распарсить его. В результате появится полная копия объекта:
const clonedCart = JSON.parse(JSON.stringify(itemsInCart))console.log(itemsInCart[1] === clonedCart[1])// false
const clonedCart = JSON.parse(JSON.stringify(itemsInCart))
console.log(itemsInCart[1] === clonedCart[1])
// false

У этого метода есть свои ограничения — копируемые данные должны быть сериализуемы (Serializable).
Вот примеры несериализуемых значений: undefined, функция, Symbol.
Массивы и объекты - сериализуемы. Что будет если у них в качестве ключа или значения будут несериализуемые данные?
- для массивов: такие значения будут превращены в null;
- для объектов: такие значения будут опущены, а если symbol является ключом объекта, то он будет проигнорирован, даже при использовании функции
replacer.
Подробнее об ограничениях JSON можно прочитать в статье JSON
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)
// {}
Метод cloneDeep()
СкопированоМожно воспользоваться готовыми решениями, например, методом _ из библиотеки Lodash. Он надёжен и используется в десятках тысяч проектов каждый день. Кстати, изучить его реализацию можно на GitHub.
import cloneDeep from 'lodash.clonedeep'const clonedCart = cloneDeep(itemsInCart)console.log(itemsInCart[1] === clonedCart[1])// false
import cloneDeep from 'lodash.clonedeep'
const clonedCart = cloneDeep(itemsInCart)
console.log(itemsInCart[1] === clonedCart[1])
// false
В отличии от structured и копирования с помощью JSON, метод clone не вызовет ошибки при копировании объекта содержащего функции, Symbol-значения или циклические ссылки.
clone корректно сохраняет ссылку на прототип исходного объекта. Это может быть важным при копировании объекта-экземпляра класса:
import cloneDeep from 'lodash.clonedeep'class Person { constructor(name) { this.name = name }}const person = new Person('Адам')// Создадим копию объекта с помощью cloneDeep()const clonedPerson1 = cloneDeep(person)// Проверим принадлежность копии объекта к классу Personconsole.log(clonedPerson1 instanceof Person)// true// Создадим копию объекта с помощью structuredClone()const clonedPerson2 = structuredClone(person)// Проверим принадлежность копии объекта к классу Personconsole.log(clonedPerson2 instanceof Person)// false
import cloneDeep from 'lodash.clonedeep'
class Person {
constructor(name) {
this.name = name
}
}
const person = new Person('Адам')
// Создадим копию объекта с помощью cloneDeep()
const clonedPerson1 = cloneDeep(person)
// Проверим принадлежность копии объекта к классу Person
console.log(clonedPerson1 instanceof Person)
// true
// Создадим копию объекта с помощью structuredClone()
const clonedPerson2 = structuredClone(person)
// Проверим принадлежность копии объекта к классу Person
console.log(clonedPerson2 instanceof Person)
// false
Метод clone имеет некоторые ограничения. По ссылке сохраняются:
- DOM-элементы;
Symbol;Function;Weak-объекты;Map Weak-объекты;Set Proxy-объекты.