- PVSM.RU - https://www.pvsm.ru -
Вы знали, что теперь в JavaScript есть нативный способ делать глубокие копии объектов? Это стало возможным с помощью функции structuredClone
, встроенной в среду выполнения JavaScript:
const calendarEvent = {
title: "Builder.io Conf",
date: new Date(123),
attendees: ["Steve"]
}
//
const copied = structuredClone(calendarEvent)
Вы заметили, что в этом примере мы скопировали не только объект, но и вложенный массив, и даже объект Date?
И код работает именно так, как мы и ожидали:
copied.attendees // ["Steve"]
copied.date // Date: Wed Dec 31 1969 16:00:00
cocalendarEvent.attendees === copied.attendees // false
structuredClone
может делать не только вышеперечисленное, но и также:
Клонировать бесконечно вложенные объекты и массивы.
Клонировать циклические ссылки.
Клонировать широкий спектр типов JavaScript, таких как: Date
, Set
, Map
, Error
, RegExp
, ArrayBuffer
, Blob
, File
, ImageData
и многие другие [1].
Передавать любые передаваемые объекты [2].
Это безумие даже будет работать так, как мы и ожидали:
const kitchenSink = {
set: new Set([1, 3, 3]),
map: new Map([[1, 2]]),
regex: /foo/,
deep: { array: [ new File(someBlobData, 'file.txt') ] },
error: new Error('Hello!')
}
kitchenSink.circular = kitchenSink
//
Выполнено полное глубокое копирование
const clonedSink = structuredClone(kitchenSink)
Важным отметить, что мы говорим о глубоком копировании. Если же нужно просто выполнить поверхностное копирование, то есть копирование без включения вложенных объектов или массивов, то можно просто выполнить Оператор spread (три точки — ...) используется для извлечения отдельных элементов из массива</p>" data-abbr="spread объекта">spread объекта:
const simpleEvent = {
title: "Builder.io Conf",
}
//
нет вложенных объектов или массивов
const shallowCopy = {...calendarEvent}
Или даже один из этих вариантов, если хотите:
const shallowCopy = Object.assign({}, simpleEvent)
const shallowCopy = Object.create(simpleEvent)
Но как только появляются вложенные элементы, мы сталкиваемся с проблемой:
const calendarEvent = {
title: "Builder.io Conf",
date: new Date(123),
attendees: ["Steve"]
}
const shallowCopy = {...calendarEvent}
//
упс - мы добавили "Bob" и в копию и в воригинальное событие
shallowCopy.attendees.push("Bob")
//
упс - мы обновили дату копии и исходного события
shallowCopy.date.setTime(456)
Как видно, мы не сделали полную копию этого объекта.
Вложенные дата и массив по-прежнему являются общей ссылкой для оригинала и «копии». Это может привести к проблеме – если мы захотим отредактировать их, думая, что обновляем только скопированный объект события календаря.
На самом деле это отличный хак и на удивление производительный, но с некоторыми недостатками, которые устраняет structuredClone
.
Возьмем для примера:
const calendarEvent = {
title: "Builder.io Conf",
date: new Date(123),
attendees: ["Steve"]
}
//
JSON.stringify преобразовал дату в строку
const problematicCopy = JSON.parse(JSON.stringify(calendarEvent))
Если вывести ProblematicCopy
, мы получим:
{
title: "Builder.io Conf",
date: "1970-01-01T00:00:00.123Z"
attendees: ["Steve"]
}
Мы хотели не этого. date
должен быть не строкой, а объектом Date
.
Это произошло потому, что JSON.stringify
может обрабатывать только базовые объекты, массивы и примитивы. Любой другой тип может быть обработан непредсказуемым образом. Например, Dates преобразуются в string. Но Set
просто преобразуется в {}
.
Что-то JSON.stringify
даже игнорирует – например, undefined
или функции.
Скажем, если мы скопируем пример kitchenSink
с помощью этого метода:
const kitchenSink = {
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 veryProblematicCopy = JSON.parse(JSON.stringify(kitchenSink))
То мы получим:
{
"set": {},
"map": {},
"regex": {},
"deep": {
"array": [
{}
]
},
"error": {},
}
Фу!
И да, пришлось удалить циклическую ссылку, которая у нас изначально для этого была, поскольку JSON.stringify
просто выдает ошибки, если встречается с одной из них.
Метод JSON.stringify
удобен, в случае если наши требования соответствуют его возможностям. Однако с помощью StructuredClone
можно сделать многое из того, чего не может JSON.stringify
.
До сих пор распространенным решением этой проблемы была функция cloneDeep
библиотеки Lodash.
Она действительно работает так, как ожидается:
import cloneDeep from 'lodash/cloneDeep'
const calendarEvent = {
title: "Builder.io Conf",
date: new Date(123),
attendees: ["Steve"]
}
//
Все в порядке
const clonedEvent = structuredClone(calendarEvent)
Но с одной оговоркой. Согласно данным работы расширения Import Cost [3] в IDE, которое выводит вес в Кб всего, что я импортирую, эта функция занимает 17,4 Кб в сжатом виде (5,3 Кб в архиве):
Это предполагает, что вы импортируете только эту функцию. Если вместо этого импортировать более распространенным способом, не принимая в расчет, что 25 Кб [4] только для этой одной функции.
Хотя это и не станет концом света, в нашем случае это просто не нужно – не тогда, когда браузеры уже имеют встроенный structuredClone
.
Иначе они вызовут исключение DataCloneError
:
//
Ошибка!
structuredClone({ fn: () => { } })
Также выбрасывают исключение DataCloneError
:
//
Ошибка!
structuredClone({ el: document.body })
Также не клонируются аналогичные метадата-подобные фичи.
К примеру, при использовании геттера клонируется результирующее значение, но не сама функция геттера (или любые другие метаданные свойства):
structuredClone({ get foo() { return 'bar' } })
// Становится: { foo: 'bar' }
Не происходит обход цепочки прототипов. Поэтому в случае клонирования экземпляра MyClass
клонированный объект больше не будет известен как экземпляр этого класса. Но все валидные свойства этого класса будут клонированы.
class MyClass {
foo = 'bar'
myMethod() { /* ... */ }
}
const myClass = new MyClass()
const cloned = structuredClone(myClass)
// Становится: { foo: 'bar' }
cloned instanceof myClass // ложь
Все, что не входит в приведенный ниже список, клонировать нельзя:
Array
, ArrayBuffer
, Boolean
, DataView
, Date
, Error
types (указанные в списке ниже), Map
, Object
(но только простые объекты – например, из объектных литералов), примитивные типы (за исключением symbol
– number
, string
, null
, undefined
, boolean
, BigInt
), RegExp
, Set
, TypedArray
Error
, EvalError
, RangeError
, ReferenceError
, SyntaxError
, TypeError
, URIError
AudioData
, Blob
, CryptoKey
, DOMException
, DOMMatrix
, DOMMatrixReadOnly
, DOMPoint
, DomQuad
, DomRect
, File
, FileList
, FileSystemDirectoryHandle
, FileSystemFileHandle
, FileSystemHandle
, ImageBitmap
, ImageData
, RTCCertificate
, VideoFrame
И здесь самое интересное – structuredClone
поддерживается во всех основных браузерах, и даже в Node.js и Deno.
Правда, с одной оговоркой – поддержка Web Workers более ограничена:
Источник: MDN [5]
Мы долго этого ждали, и теперь у нас наконец-то есть structuredClone
, благодаря которому глубокое клонирование объектов в JavaScript становится простым делом. Спасибо, Surma [6].
В заключение статьи приглашаем на открытое занятие «Прототипное наследование в JavaScript», которое состоится завтра вечером. На занятии мы разберемся, что такое прототипное наследование и как оно может помочь при разработке программ. В результате вы лучше поймете объектную модель Javascript и сможете писать ООП код с экономией памяти. Запись на урок открыта по ссылке. [7]
Автор: Ксения Мосеенкова
Источник [8]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/383150
Ссылки в тексте:
[1] многие другие: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm#supported_types
[2] передаваемые объекты: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Transferable_objects
[3] Import Cost: https://marketplace.visualstudio.com/items?itemName=wix.vscode-import-cost
[4] 25 Кб: https://bundlephobia.com/package/lodash@4.17.21
[5] MDN: https://developer.mozilla.org/en-US/docs/Web/API/structuredClone
[6] Surma: https://web.dev/structured-clone/
[7] открыта по ссылке.: https://otus.pw/5HUF/
[8] Источник: https://habr.com/ru/post/719460/?utm_source=habrahabr&utm_medium=rss&utm_campaign=719460
Нажмите здесь для печати.