JavaScript: Глубокое копирование и structuredClone

Источник: «Deep-copying in JavaScript using structuredClone»
Долгое время приходилось прибегать к обходным путям и библиотекам для создания глубокой/deep копии значения JavaScript. Появилась поддержка глубокого копирования встроенной функций 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() и, в частности, приведённый выше шаблон, чтобы сделать его максимально быстрым. И хотя он быстрый у него есть пара недостатков и ловушек:

Структурированное клонирование

Платформе уже требовалась возможность создавать глубокие копии значений JavaScript в нескольких местах: для хранения значений JS в IndexedDB требуется некоторая форма сериализации, чтобы его можно было сохранить на диске, а затем десериализовать для восстановления значения JS. Точно так же отправка сообщений WebWorker через postMessage() требует передачи значений JS из одной области JS в другую. Алгоритм, который используется для этого, называется Структурированное клонирование и до недавнего времени не был легко доступным для разработчиков.

Теперь это изменилось! В спецификацию HTML были внесены поправки, что бы предоставить функцию под названием structuredClone(), которая запускает именно этот алгоритм, что бы разработчики могли легко создавать глубокие копии значений JavaScript.

const myDeepCopy = structuredClone(myOriginal);

Вот и всё! Вот и весь API! Если хотите получить более детальную информацию, прочитайте статью на MDN

Особенности и ограничения

Структурированное клонирование устраняет многие (хотя и не все) недостатки техники JSON.stringify(). Структурированное клонирование может работать с циклическими структурами данных, поддерживает множество встроенных типов данных и, как правило, является более надёжным, а часто и более быстрым.

Однако, у него есть некоторые ограничения, которые могут застать вас врасплох:

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

Производительность

Хотя я ещё не проводил нового сравнения микротестов, я провёл сравнение в начале 2018 года, до того, как structuredClone() был представлен. Тогда JSON.parse() был самым быстрым вариантом для очень маленьких объектов. Я ожидаю, что это так и останется. Методы, основанные на структурированном клонировании были (значительно) быстрее для больших объектов. Учитывая, что новый structuredClone() поставляется без излишнего злоупотребления другими API и более надёжен, чем JSON.parse(). Я рекомендую использовать его по умолчанию для создания глубоких копий.

Вывод

Если нужно создать глубокую копию значения в JavaScript — возможно, это потому, что вы используете неизменяемые структуры данных, или хотите убедиться, что функция может манипулировать объектом не затрагивая оригинал — вам больше не нужно искать обходные пути или библиотеки. Теперь экосистема JavaScript содержит structuredClone(). Ура.

Дополнительные материалы

Предыдущая Статья

Laravel: Получение информации о пользователе

Следующая Статья

Laravel: подключение Tailwind CSS 2