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

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

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

Browser Support

  • Хром: 98.
  • Край: 98.
  • Firefox: 94.
  • Сафари: 15.4.

Source

Неглубокие копии

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

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

MDN — Примитивный

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

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

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

Раньше не было простого или удобного способа создать глубокую копию значения в JavaScript. Многие полагались на сторонние библиотеки, такие как функция lodash cloneDeep() . Пожалуй, наиболее распространенным решением этой проблемы был хак на основе 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 в другую. Алгоритм, который используется для этого, называется «Structured Clone» и до недавнего времени не был легкодоступен для разработчиков.

Теперь это изменилось! Спецификация 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() . Ура.