Кратко
СкопированоПри копировании объекта, его примитивные свойства (числа, строки, булевы значения) дублируются, а вложенные объекты (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 proto = Object.getPrototypeOf(object) if (proto !== Object.prototype || proto !== null) { 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 proto = Object.getPrototypeOf(object)
if (proto !== Object.prototype || proto !== null) {
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 deep = structuredClone(itemsInCart)console.log(itemsInCart[1] === deep[1])// false
const deep = structuredClone(itemsInCart)
console.log(itemsInCart[1] === deep[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-объекты.