JavaScript: Глубокое копирование и structuredClone
structuredClone()
.На момент написания этой статьи все браузеры реализовали этот API в своих ночных релизах, Firefox выпустил в стабильной версии Firefox 94. Кроме того, этот API реализован в Node 17 и Deno 1.14. Вы можете начать использовать эту функцию прямо сейчас.
Поверхностные копии
Копирование значения в JavaScript почти всегда происходит поверхностно, в отличие от глубокого. Это означает, что изменения глубоко вложенных значений будут видны как в копии, так и в оригинале.
Один из способов создания поверхностной копии в JavaScript с помощью оператора spread ...
:
const myOriginal = {
someProp: "with a string value",
anotherProp: {
withAnotherProp: 1,
andAnotherProp: true
}
};
const myShallowCopy = {...myOriginal};
Добавление или изменение свойства непосредственно в поверхностной копии повлияет только на копию, а не на оригинал.
myShallowCopy.aNewProp = "a new value";
console.log(myOriginal.aNewProp)
// ^ logs `undefined`
Однако добавление или изменение глубоко вложенного свойства влияет как на копию, так и на оригинал:
myShallowCopy.anotherProp.aNewProp = "a new value";
console.log(myOriginal.anotherProp.aNewProp)
// ^ logs `a new value`
Выражение {...myOriginal}
перебирает (перечисляемые) свойства MyOriginal
с помощью spread оператора. Он использует имя и значение свойства присваивая их одно за другим только что созданному пустому объекту. Таким образом итоговый объект идентичен по форме, но имеет собственную копию списка свойств и значений. Значения тоже копируются, но так называемые примитивные значения обрабатываются JavaScript иначе, чем не примитивные значения. Цитирую MDN — Primitive:
Примитив (значение примитивного типа, примитивный тип данных) это данные, которые не являются объектом и не имеют методов. В JavaScript 7 простых типов данных: string, number, boolean, null, undefined, symbol (новое в ECMAScript 2015), bigint.
Не примитивные значения обрабатываются как ссылки, а это означает, что копирование значения на самом деле просто копирует ссылку на тот же основополагающий объект, что приводит к поведению поверхностного копирования.
Глубокие копии
Противоположностью поверхностного копирования является глубокое копирование. Алгоритм глубокого копирования так же копирует свойства объекта одно за другим, но рекурсивно вызывает себя, когда находит ссылку на другой объект, создавая так же копию этого объекта. Это может быть очень важно, быть уверенным, что два фрагмента кода случайно не используют один и тот же объект и неосознанно не манипулируют состоянием друг друга.
Раньше не было простого способа создания глубокой копии значения JavaScript. Многие полагались на сторонние библиотеки, такие как функция cloneDeep()
Lodash. Возможно наиболее распространённым решением этой проблемы был хак на основе JSON:
const myDeepCopy = JSON.parse(JSON.stringify(myOriginal));
Это было настолько популярное решение, что V8 агрессивно оптимизировал JSON.parse()
и, в частности, приведённый выше шаблон, чтобы сделать его максимально быстрым. И хотя он быстрый у него есть пара недостатков и ловушек:
- Рекурсивные структуры данных:
JSON.stringify()
вызовет исключение, когда вы дадите ему рекурсивную структуру данных. Это может легко произойти при работе со связными списками или деревьями - Встроенные типы:
JSON.stringify()
вызовет исключение, если значение содержит другие встроенные функции JavaScript, такие, какMap
,Set
,Date
,RegExp
илиArrayBuffer
. - Функции:
JSON.stringify()
молча отбрасывает функции.
Структурированное клонирование
Платформе уже требовалась возможность создавать глубокие копии значений JavaScript в нескольких местах: для хранения значений JS в IndexedDB требуется некоторая форма сериализации, чтобы его можно было сохранить на диске, а затем десериализовать для восстановления значения JS. Точно так же отправка сообщений WebWorker через postMessage()
требует передачи значений JS из одной области JS в другую. Алгоритм, который используется для этого, называется Структурированное клонирование
и до недавнего времени не был легко доступным для разработчиков.
Теперь это изменилось! В спецификацию HTML были внесены поправки, что бы предоставить функцию под названием structuredClone()
, которая запускает именно этот алгоритм, что бы разработчики могли легко создавать глубокие копии значений JavaScript.
const myDeepCopy = structuredClone(myOriginal);
Вот и всё! Вот и весь API! Если хотите получить более детальную информацию, прочитайте статью на MDN
Особенности и ограничения
Структурированное клонирование устраняет многие (хотя и не все) недостатки техники JSON.stringify()
. Структурированное клонирование может работать с циклическими структурами данных, поддерживает множество встроенных типов данных и, как правило, является более надёжным, а часто и более быстрым.
Однако, у него есть некоторые ограничения, которые могут застать вас врасплох:
- Прототипы: Если вы используете
structuredClone()
с экземпляром класса, вы получите простой объект в качестве возвращаемого значения, поскольку структурированное копирование отбрасывает цепочку прототипов объекта. - Функции: Если ваш объект содержит функции, они будут тихо отброшены.
- Не клонируемые: Некоторые значения не структурированы для клонирования, в первую очередь это
Error
иDOM
узлы. Это приведёт к тому, чтоstructuredClone()
вызовет исключение.
Если какое-либо из этих ограничений является препятствием для вашего использования, то такие библиотеки как Lodash, по-прежнему предоставляют сторонние реализации других алгоритмов глубоко копирования, которые могут соответствовать вашему варианту использования.
Производительность
Хотя я ещё не проводил нового сравнения микротестов, я провёл сравнение в начале 2018 года, до того, как structuredClone()
был представлен. Тогда JSON.parse()
был самым быстрым вариантом для очень маленьких объектов. Я ожидаю, что это так и останется. Методы, основанные на структурированном клонировании были (значительно) быстрее для больших объектов. Учитывая, что новый structuredClone()
поставляется без излишнего злоупотребления другими API и более надёжен, чем JSON.parse()
. Я рекомендую использовать его по умолчанию для создания глубоких копий.
Вывод
Если нужно создать глубокую копию значения в JavaScript — возможно, это потому, что вы используете неизменяемые структуры данных, или хотите убедиться, что функция может манипулировать объектом не затрагивая оригинал — вам больше не нужно искать обходные пути или библиотеки. Теперь экосистема JavaScript содержит structuredClone()
. Ура.