Кирилл Мыльников
Фронтенд-разработчик
Всем привет, я Кирилл, фронтенд-разработчик в Usetech.
Сегодня мы поговорим о глубоком и поверхностном клонировании объекта, увидим различные примеры и способы его реализации, а также разберем отличия, преимущества и недостатки данного подхода, обратим внимание на новый метод комплексного глубокого клонирования – structuredClone
.
Глубокое клонирование:
Клонирование поверхности:
-
Оператор спреда
-
Объект.назначить()
-
Объект.создать()
Знаете ли вы, что теперь в JavaScript есть встроенный способ создания глубоких копий объекта?
Итак, мы говорим о structuredClone
это функция, встроенная в среду JavaScript:
const person = {
name: "Ivan",
date: new Date(123),
friends: ["John"]
}
// клон объекта
const copiedPerson = structuredClone(person)
Обратите внимание, что мы скопировали не только объект, но и вложенный массив, и даже объект Date.
Как видите, все работает, как и ожидалось.
copiedPerson.friends // ["John"]
copiedPerson.date // Date: Wed Dec 31 1969 16:00:00
copiedPerson.friends === person.friends // false
Метод structuredClone
может выполнять следующие задачи:
-
Клонировать бесконечно вложенные объекты и массивы;
-
Клонировать циклические ссылки;
-
Клонируйте различные типы JS, такие как
Date
,Error
,RegExp
,ArrayBuffer
,Blob
,File
,ImageData
И другой; -
Передача всех переданные объекты.
Даже такой сумасшедший пример сработал так, как мы хотели.
const obj = {
set: new Set([1123, 36, 3, 4, 4]),
map: new Map([[133, 2222]]),
regex: /foo/,
deep: { array: [ new File(someBlobData, 'test.txt') ] },
error: new Error('Error!')
}
obj.circular = obj
// ✅ Все хорошо, клонировали весь объект.
const clonedObj = structuredClone(obj)
Почему бы просто не взять и не катить объект?
Важно понимать, какого типа клонирование мы хотим: глубокое или поверхностное. Если нам нужно создать поверхностный клон, т.е. копию, которая не копирует вложенные объекты или массивы, мы можем это сделать Spread
оператор.
Пример:
const obj = {
title: "Builder.io Conf",
}
// ✅ нет проблем, нет вложенных массивов и объектов
const shallowCopy = {...obj}
Есть еще один поверхностный метод клонирования:
const shallowCopy = Object.assign({}, obj);
const shallowCopy = Object.create(obj);
Возьмем пример, где у нас есть вложенные значения — это объект.
const personObj = {
title: "Hello world",
date: new Date(123),
friends: ["John"]
}
const shallowCopy = {...calendarEvent}
// 🚩 Мы только что добавили “Bob” и к копии и оригиналу
shallowCopy.friends.push("Bob")
// 🚩 Мы только что изменили время у клона и оригинала
shallowCopy.date.setTime(456)
Как видите, мы не сделали полную копию этого объекта. Вложенная дата и массив всегда являются общей ссылкой между ними, что может вызвать серьезные проблемы, если вы не различаете поверхностное и глубокое клонирование.
И может делать глубокое клонирование через JSON.parse(JSON.stringify(obj))
?
Этот трюк удивительно продуктивен, но имеет несколько недостатков, которые structuredClone
устраняет.
Возьмите этот объект в качестве примера:
const person = {
name: "Ivan",
date: new Date(123),
friends: ["John"]
}
// 🚩 Выполним глубокое клонирование.
const problematicPersonCopy = JSON.parse(JSON.stringify(person))
Если мы посмотрим на результат problematicPersonCopy
мы увидим следующее:
{
name: "Ivan",
date: "1970-01-01T00:00:00.123Z"
friends: ["John"]
}
Это немного отличается от того, что мы хотели: date должен быть объектом Date, а не строкой. Это произошло потому, что JSON.stringify()
может обрабатывать только базовые объекты, массивы, примитивы. Со всеми другими типами он может вести себя не так, как вы ожидаете. Например, дата будет преобразована в строку, а набор — в {}. JSON.stringify()
полностью игнорирует некоторые вещи, такие как undefined или functions.
Пример:
const obj = {
set: new Set([1, 3, 3]),
map: new Map([[1, 2]]),
regex: /foo/,
deep: { array: [ new File(someBlobData, 'file.txt') ] },
error: new Error('Hello!')
}
const veryProblematicObjCopy = JSON.parse(JSON.stringify(obj))
Что мы получим:
{
"set": {},
"map": {},
"regex": {},
"deep": {
"array": [
{}
]
},
"error": {},
}
Делаем вывод, что этот способ поможет вам только при наличии базовых объектов, массивов, примитивов.
Рассмотрим следующий метод через lodash.cloneDeep.
Сегодня cloneDeep
функция lodash
очень распространенное решение этой проблемы.
И действительно работает так, как ожидалось:
import cloneDeep from 'lodash/cloneDeep'
const person = {
name: "Ivan",
date: new Date(123),
friends: ["John"]
}
// ✅ Все хорошо!
const clonedEvent = cloneDeep(person)
Но здесь есть оговорка. В зависимости от расширения Стоимость импортав моей IDE, которая отображает вес в килобайтах того, что я импортирую, эта функция занимает всего 17,4 КБ.

Если не придавать значения и сразу импортировать из библиотеки, то мы увидим, насколько импорт стал сложнее. И это только для одной функции. cloneDeep
.

Но перетаскивать целую библиотеку для глубокого клонирования пока не нужно, когда есть structuredClone
.
Полифил клонГлубокий
Кроме того, мы всегда можем написать собственный полифил, если это абсолютно необходимо для глубокого клонирования.
Пример полифила:
const deepClone = (obj) => {
if (obj === null) return null
const clone = Object.assign({}, obj)
Object.keys(clone).forEach(
(key) =>
(clone[key] =
typeof obj[key] === 'object' ? deepClone(obj[key]) : obj[key])
)
if (Array.isArray(obj)) {
clone.length = obj.length
return Array.from(clone)
}
return clone
}
const a = { foo: 'bar', obj: { a: 1, b: 2 } }
const b = deepClone(a) // a !== b = true, a.obj === b.obj = false
Реализация довольно простая, но если вы считаете, что я что-то упустил, вы можете отредактировать и рассказать об этом в комментариях.
Что в structuredClone
нельзя клонировать?
1. Функции
Они вызовут исключение DataCloneError.
// 🚩 Ошибка!
structuredClone({ fn: () => { } })
2. Узлы ДОУ
Также вызывает исключение DataCloneError.
// 🚩 Ошибка!
structuredClone({ el: document.body })
3. Дескрипторы свойств, сеттеры и геттеры. А также подобные функции, такие как метаданные, не клонируются
Например, с геттером клонируется результирующее значение, но не сама функция геттера.
structuredClone({ get foo() { return 'bar' } })
4. Прототипы объектов.
Если вы запланируете экземпляр myClass, клонированный объект больше не будет известен как экземпляр этого класса (но будут клонированы все фактические свойства этого класса).
class MyClass {
foo = 'bar'
myMethod() { /* ... */ }
}
const myClass = new MyClass()
const cloned = structuredClone(myClass)
// Becomes: { foo: 'bar' }
cloned instanceof myClass // false
Полный список поддерживаемых типов. Проще говоря, то, чего нет в списке типов, клонировать нельзя:
Встроенные модули JS: Array, ArrayBuffer, Boolean, DataView, Date, Error, Map и т.д.
Типы ошибок: Error, EvalError, RangeError, ReferenceError, SyntaxError, TypeError и т.д.
Поддержка браузера и исполнение.
И вот лучшая часть – structuredClone
поддерживается во всех основных браузерах, даже в Node.js и Deno.
Поддержка браузера: Источник: Не беспокоить

Мы рассмотрели глубокое и поверхностное клонирование, проанализировали примеры и методы и выделили плюсы и минусы. Если я что-то упустил, вы можете добавить меня в комментарии или рассказать о своем опыте и поделиться примерами.