Глубокое копирование в JavaScript с использованием структурированного клона

Платформа теперь поставляется со структурированной Clone(), встроенной функцией глубокого копирования.

Долгое время вам приходилось прибегать к обходным путям и библиотекам для создания глубокой копии значения JavaScript. Платформа теперь поставляется со structuredClone() — встроенной функцией глубокого копирования.

Поддержка браузера

  • Хром: 98.
  • Край: 98.
  • Фаерфокс: 94.
  • Сафари: 15.4.

Источник

Мелкие копии

Копирование значения в JavaScript почти всегда является поверхностным , в отличие от глубокого . Это означает, что изменения глубоко вложенных значений будут видны как в копии, так и в оригинале.

Один из способов создать неглубокую копию в JavaScript с помощью оператора расширения объекта ... :

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 с помощью оператора расширения . Он использует имя и значение свойства и присваивает их одно за другим только что созданному пустому объекту. Таким образом, результирующий объект идентичен по форме, но имеет собственную копию списка свойств и значений. Значения также копируются, но так называемые примитивные значения обрабатываются значением JavaScript иначе, чем непримитивные значения. Цитирую MDN :

В JavaScript примитив (примитивное значение, примитивный тип данных) — это данные, которые не являются объектом и не имеют методов. Существует семь примитивных типов данных: строка, число, bigint, логическое значение, неопределенное, символ и ноль.

MDN — Примитив

Непримитивные значения обрабатываются как ссылки . Это означает, что копирование значения на самом деле представляет собой просто копирование ссылки на тот же базовый объект, что приводит к неглубокому копированию.

Глубокие копии

Противоположностью поверхностной копии является глубокая копия. Алгоритм глубокого копирования также копирует свойства объекта одно за другим, но вызывает себя рекурсивно, когда находит ссылку на другой объект, создавая также копию этого объекта. Это может быть очень важно, чтобы убедиться, что два фрагмента кода случайно не используют общий объект и неосознанно не манипулируют состоянием друг друга.

Раньше не существовало простого и приятного способа создать глубокую копию значения в JavaScript. Многие люди полагались на сторонние библиотеки, такие как функция cloneDeep() от Lodash . Вероятно, наиболее распространенным решением этой проблемы был взлом на основе JSON:

const myDeepCopy = JSON.parse(JSON.stringify(myOriginal));

Фактически, это был настолько популярный обходной путь, что V8 агрессивно оптимизировал JSON.parse() и, в частности, приведенный выше шаблон, чтобы сделать его максимально быстрым. И хотя он быстрый, у него есть пара недостатков и проблем:

  • Рекурсивные структуры данных : JSON.stringify() выдаст ошибку, когда вы зададите ему рекурсивную структуру данных. Это может произойти довольно легко при работе со связанными списками или деревьями.
  • Встроенные типы : JSON.stringify() выдаст исключение, если значение содержит другие встроенные элементы JS, такие как 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() с экземпляром класса, вы получите простой объект в качестве возвращаемого значения, поскольку структурированное клонирование отбрасывает цепочку прототипов объекта.
  • Функции : если ваш объект содержит функции, structuredClone() выдаст исключение DataCloneError .
  • Неклонируемые : некоторые значения не структурированы для клонирования, особенно узлы Error и DOM. Это приведет к вызову structuredClone() .

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

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

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

Заключение

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