Кирилл Мыльников
Frontend разработчик
Всем привет, я — Кирилл, frontend разработчик компании Usetech.
Сегодня поговорим о глубоком и поверхностном клонировании объекта, посмотрим различные примеры и способы как это можно реализовать, а также разберём отличия, плюсы и минусы данного подхода, уделим внимание новому встроенному методу глубокого клонирования — structuredClone
.
Глубокое клонирование:
structuredClone(obj)
lodash.cloneDeep(obj)
JSON.parse(JSON.stringify(obj))
Полифил cloneDeep(obj)
Поверхностное клонирование:
Оператор Spread
Object.assign()
Object.create()
Знали ли вы, что теперь в 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()
может обрабатывать только базовые объекты, массивы, примитивы. С любыми другими типами он может повести себя не так, как бы вы хотели. Например, дату преобразует в строку, а Set преобразует в {}. JSON.stringify()
полностью игнорирует некоторые вещи, такие как undefined или функции.
Пример:
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)
Но здесь есть одна оговорка. Согласно расширению Import Cost, в моей IDE, которое показывает вес в килобайтах того, что импортирую, эта функция занимает всего 17.4К.
Если не придавать значения и сразу импортировать из библиотеки, то мы увидим, насколько тяжелее стал импорт. И это только для одной функции cloneDeep
.
Но затаскивать целую библиотеку для глубокого клонирования в данный момент уже не нужно, когда есть structuredClone
.
Полифил cloneDeep
Также мы всегда можем написать свой собственный полифил, если это крайне необходимо для глубокого клонирования.
Пример полифила:
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. DOW-узлы.
Также выдает 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.
Поддержка браузеров: Источник: MDN
Мы рассмотрели глубокое и поверхностное клонирование, разобрали примеры и способы, выделили преимущества и недостатки. Если я что-то упустил, можете дополнить меня в комментариях или рассказать о своём опыте и поделиться примерами.
Комментарии (22)
Alexandroppolus
17.04.2023 06:47+3const shallowCopy = Object.create(obj);
разумеется, это никакой не shallowCopy.
const obj = {x: 1}; const shallowCopy = Object.create(obj); obj.x = 2; console.log(shallowCopy.x); // 2
gmtd
17.04.2023 06:47Метод structuredClone может выполнять следующие задачи:
- Клонировать бесконечно вложенные объекты и массивы;
За бесконечное время? )
xaosxaos2
17.04.2023 06:47А чем эта статья отличается от ранее уже бывшей? https://habr.com/ru/companies/otus/articles/719460/
askolo4ek
Большое спасибо за статью! Недавно как раз начал изучать JS. Узнал про методы копирования объекта через Object.assign и JSON.parse(...), но мне показалось что и то и то работает криво.. А вот structuredClone теперь буду применять на практике
nin-jin
Не знаю, зачем эту функцию добавили в спеку, но ажиотаж вокруг этого антипаттерна подрывает мою веру в человечество.
askolo4ek
По моему ощущению, весь JS - сплошной антипаттерн (да не в обиду будет сказано). Потому что конкатенация строки с массивом взрывает мозг. Создание переменной из области видимости функции в глобальную видимость - это вообще за гранью добра и зла
Opaspap
Это как ? Вряд ли вы сможете объявить переменную именно в более высоком скопе (не считая момента, чио после появления let у нас два скопа теперь, и можно сказать , что объявляя var в фигурных скобочках мы выходим за скоп, но это как то жиденько), а не свойство объекта window (или global), что и во многих других языках вполне легально.
Alexandroppolus
Наверно, речь про то, что если не в строгом режиме присвоить значение несуществующей переменной, то она создается в глобальном скоупе. В строгом это будет ошибка.
askolo4ek
Верно, спасибо за уточнение
Opaspap
Правда в том, что она не создается в этом скопе, т.к. в таком случае она присвоится window (в отличии от объявления с помошью var, в верхнем скопе) и обращение к ней пойдет по схеме "нет ни в одном в скопе, а есть ли свойство window ?", Т.е. это не переменная, а такой способ объявления, просто сахар для обращения к window.
И да, уже много лет рекомендуется всегда использовать "use strict", чтобы выключить это "фичу" от тех времён, когда никому не могло придти в голову, что нацдутся сумасшедшие, которые будут несколько раз в минуту качать приложения по. 100кб и запускать их на клиентских устройствах. Интересно, как в minitel фронтенд был сделан, кто нибудь знает ?
askolo4ek
Ниже уже более детально пояснили. Но в целом JS мне нравится своей простотой. На нём можно чё-то быстро налабать, и будеть работать
nin-jin
Но работать будет не правильно, поэтому придётся долго-долго отлаживать.
inoyakaigor
А что с ним не так?
nin-jin
Отсутствие разумных кейсов применения в прикладном коде.
Gary_Ihar
Можно подробнее пожалуйста? Или скажите тему почитать. Интересна Ваша идея
nin-jin
Во всех случаях, когда эту функцию можно выло бы применить, есть решения лучше, не требующие полного копирования.
Gary_Ihar
Ну так то да. Заметил, что я за три с половиной года(весь мой опыт, так что прошу строго не судить) ни разу и не копировал объекты глубоко, кроме одного момента.
Вот есть форма редактирования чего-либо. Вот мы ее открываем и закидываем в поля глубоко скопированные данные чтобы их там в форме менять и т.д. Но в уме(любое js хранилище) держим оригинал, что бы иметь возможность сделать to_default_state_action. Это наверное единственное место, где я пока не могу придумать как обойтись без глубокого копирования.
Как бы Вы поступили в данном случае?
nin-jin
Много вариантов:
Copy-On-Write прокси.
Писать в отдельный объект, читать из обоих.
Хранить изменённое состояние в компонентах, при сабмите собирать и посылать, при ресете ререндерить.
Gary_Ihar
Мб я не дополнял, но по моему я иду всегда по третьему Вашему пункту :) я считаю это за глубокое копирование, да это не вызов cloneDeep, но все таки это парсинг данных из оригинала в состояние формы, которое все таки повторяет структуру оригинала(не всегда, но часто). По итогу и выйдет, что есть оригинал и его копия в форме. Я не прав ? Если да, то поправьте плз
nin-jin
Не копия, а зависимое состояние, часто имеющее мало общего с оригиналом. Например, в данных число, а в форме это строка.