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

13.12.2022 642
IBS Training Center Telegram
Подписывайтесь на наш канал в Telegram:
больше материалов экспертов, анонсы бесплатных вебинаров и задачки для IT-специалистов
Подписаться

Поверхностное копирование

Копирование значения в 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 — Примитив

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

11235921_11104.jpg

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

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

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

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

Это обходное решение было настолько популярным, что в
движке JavaScript V8 был оптимизирован метод JSON.parse() и в особенности указанный выше паттерн,
чтобы сделать его работу максимально быстрой. Он действительно работает быстро,
но имеет ряд недостатков и подводных камней:
  • Рекурсивные структуры данных. Метод JSON.stringify() работает, когда ему задана рекурсивная структура данных. Это часто происходит при работе со связанными списками или деревьями.
  • Встроенные типы. Метод JSON.stringify() работает, если значение содержит другие встроенные объекты JS, такие как MapSetDateRegExp или ArrayBuffer.
Функции. Метод JSON.stringify() отклоняет функции.

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

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

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

Только и всего! Это весь API. Если вы хотите узнать подробности, посмотрите статью MDN.

Возможности и ограничения

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

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

  • Прототипы. Если вы используете structuredClone() с экземпляром класса, то в качестве возвращаемого объекта вы получает основной объект, так как структурное клонирование отменяет цепочку прототипов объекта.

  • Функции. Если ваш объект содержит функции, они будут тихо удалены.

  • Неклонируемые значения. Для некоторых значений невозможно применить структурное клонирование, прежде всего для Error и узлам DOM. Попытка приведет к тому, что structuredClone() выдаст ошибку.

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

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

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

Заключение

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

Погрузитесь в изучение Java и освойте новые технологии на продвинутом уровне со скидкой 10%: https://ibs-training.ru/training/katalog_kursov/razrabotka_po_java/

Источник: https://web.dev/structured-clone/




Расскажи друзьям:

Как не пропустить самое интересное?
Подписывайтесь на наш ежемесячный дайджест!
Спасибо.
Вы подписаны на ежемесячный дайджест.
Пользователь только что записался на курс ""
Спасибо!
Форма отправлена успешно.