вернуться назад

Клонирование объектов JS

Habr.ru
Мыльников Кирилл
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, такие, как DateErrorRegExpArrayBufferBlobFileImageData и другие;
  • Передача любых передаваемых объектов.

Даже такой безумный пример сработал, как мы и хотели.

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

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

Loading

Мы обрабатываем файлы cookie для корректной работы сайта и персонализации сервисов, в т.ч. с использованием метрических программ и систем аналитик. Продолжая пользоваться сайтом, Вы соглашаетесь с использованием файлов cookie. Если вы не хотите, чтобы ваши данные обрабатывались, пожалуйста, ограничьте использование файлов cookie в настройках браузера. Более подробная информация на странице Политика конфиденциальности.
Принять
Мы обрабатываем файлы cookie для корректной работы сайта и персонализации сервисов, в т.ч. с использованием метрических программ и систем аналитик. Продолжая пользоваться сайтом, Вы соглашаетесь с использованием файлов cookie. Если вы не хотите, чтобы ваши данные обрабатывались, пожалуйста, ограничьте использование файлов cookie в настройках браузера. Более подробная информация на странице Политика конфиденциальности.
Принять