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

Код, созданный в этом исследовании, находится в открытом доступе здесь. Взглянуть на список протестированных методов клонирования можно здесь.

Настоятельно рекомендуется просмотреть код: там есть много того, о чем не идет речь в этой статье.

Выбранные для исследования методы были получены из ответов на StackOverflow, библиотек, таких как Lodash и clone-deep, или являются нативными методами (structuredClone).

В исследовании не было уделено внимание неглубокому клонированию и вопросам производительности.


Какой метод клонирования считается эффективным?

Эффективная функция клонирования удовлетворяет двум условиям:

  • возвращает точную копию входных данных;
  • копия должна быть независима от оригинала.

Методология

Для оценки методов клонирования был проверен каждый из них с помощью нескольких тестов.

Список тестов разделен на 4 файла:

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

Общие результаты были плохими… шокирующе плохими.

Клонирование примитивных типов

# Пример того, как можно запустить тестовый файл примитивных типов
npm run test -- test/deep-clone/primitive-types.spec.js

В первом тесте, тестовом наборе primitive-types.spec.js, была предпринята попытка клонировать семь примитивных типов JavaScript: string, number, boolean, undefined, null, Symbol и BigInt.

Вывод primitive-type.spec.js для метода structuredClone: structuredClone не клонирует тип Symbol.

Обзор результатов тестирования

Таблица, отражающая результаты работы каждого метода при попытке клонировать перечисленные примитивные типы

❌ = выбранный метод не смог клонировать данный тип.

✅ = выбранный метод успешно клонировал данный тип.

Худший метод

Наименее эффективным методом клонирования оказался простой cloneJSON.

Этот метод состоит из простого JSON.parse после JSON.stringify. К сожалению, он широко используется.

# реализация cloneJSON
function cloneJSON(obj) {
return JSON.parse(JSON.stringify(obj))
}

cloneJSON  —  очень медленный метод с ограниченными возможностями. Поэтому стоит избегать его.

Неожиданность

structuredClone не может клонировать данные типа Symbol. Он выбрасывает DOMException с кодом 25, DataCloneError.

Выбрасывание ошибки кажется несколько излишним. Почему бы просто не скопировать ссылку, ведь данные типа Symbol уникальны и неизменяемы?

Этот метод появился сравнительно недавно: впервые он был реализован в Node 17. Проблемы с ним на этом не заканчиваются.

Тестовый код “should clone a Symbol”

Клонирование объектов

# Пример того, как можно запустить тестовый файл типа объекта
npm run test -- test/deep-clone/objects.spec.js

Во втором тесте была предпринята попытка клонировать объекты JavaScript, используя набор тестов objects.spec.js.

Здесь можно посмотреть, как был реализован каждый тест.

Ниже представлен обзор выполненных тестов.

Вывод objects.spec.js для метода Lodash cloneDeep. Согласно www.npmjs.com, библиотека Lodash загружается с серверов npm 37 миллионов раз в неделю

Сравним результаты каждого метода клонирования для каждого из четырех заголовков:

  • basic (базовый);
  • property descriptors (дескрипторы свойств);
  • object state (состояние объекта);
  • prototype (прототип).

Результаты тестирования: базовый

Таблица, показывающая, как каждый метод показал себя при клонировании объектов

Результаты оказались нормальными, за исключением теста на циркулярную ссылку (circular reference test), где большинство методов потерпели неудачу. Здесь можно увидеть, как реализован этот тест.

Код теста “should clone an object with circular references”

Данный тест не идеален, но позволяет многое проверить:

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

Вот ряд случайных примеров сложных ситуаций, которые этот тест не охватил:

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

Результаты тестирования: дескрипторы свойств

Таблица, показывающая, как каждый метод проявил себя при попытке клонирования свойств объекта

Все методы игнорировали геттеры, сеттеры и свойства объекта, которые можно прописывать или конфигурировать.

Лучше всего показал себя cloneLib (npm install clone)! Этот метод может клонировать неперечислимые свойства и данные типа Symbol как собственные свойства. Ни один другой метод не копирует неперечислимые свойства, что, на мой взгляд, является довольно странным.

Ни один метод не смог клонировать сеттер. Все методы на 91 строке выбросили “clone.foo is not a function” (clone.foo не является функцией”)

Результаты тестирования: состояние объекта

Таблица, показывающая, как сработал каждый метод при попытке клонировать изменения состояния, произведенные с помощью Object.preventExtensions(), Object.seal(), Object.freeze()

Ни один из методов не копирует состояние Object.freeze(), Object.seal() и Object.preventExtensions().

Код теста “should clone sealed object”

Результаты тестирования: прототип

Таблица, показывающая работу каждого метода при попытке клонирования прототипа объекта

Только три библиотеки могут правильно клонировать прототип.

Были использованы два метода:

  • structuredClone копировал прототип в новый независимый объект;
  • клоны cloneDeep и cloneLib ссылаются на тот же объект, что и во входных данных.

Оба подхода можно считать корректными, но предпочтительней, на наш взгляд, structuredClone.

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

Примечательно, что cloneDeep не может скопировать неперечислимые свойства в обычном объекте, но может сделать это внутри прототипа.

Код теста “clones the __proto__ by really copying”

Третий тест снова показывает, что structuredClone не очень хорошо работает с неперечислимыми свойствами.

Клонирование функций

# Пример того, как можно запустить тестовый файл типа функций
npm run test -- test/deep-clone/functions.spec.js

В JavaScript нет подходящего способа клонировать функцию, ни один из методов не сработал.

Результаты можно разделить на три группы.

1. Методы, которые ничего не делают, даже не создают вызываемую функцию:

  • cloneJSON;
  • structuredClone;
  • cloneTrincot;
  • cloneDeepLodash.

2. Методы, которые просто ссылаются на исходную функцию:

  • underscoreContribClone;
  • cloneNemisj;
  • cloneKoolDandy;
  • cloneRfdc;
  • justClone;
  • cloneDeep;
  • cloneLib.

3. Методы, которые, как кажется, создают независимую и вызываемую функцию, но это не так:

  • cloneFunctionsUsingBind.
Код cloneFunctionsUsingBind

Этот метод производит связанную функцию, которая является просто оберткой исходной функции.

Данный метод основан на этом ответе на StackOverflow. Ниже можете увидеть “foo” и его клон, созданный этим методом:

Оригинальная функция “foo”
Клон функции “foo”, полученный с помощью метода “foo.bind({});”

Трудно сказать, какой метод стоит использовать для клонирования функции.

Если бы клонирование функций в JavaScript было совершенно ненужным, в сети не поднималась бы эта тема.

Результаты клонирования функций были проигнорированы для получения итогового результата, приведенного ниже.

Подсчет результатов

Итоговые результаты распределены по общему количеству

Победителем стал cloneLib (npm install clone).


Итоги

  • В JavaScript нет ни одной полностью работоспособной библиотеки и метода глубокого клонирования.
  • cloneLib (npm install clone)  —  лучший метод.
  • cloneJSON не следует использовать.
  • structuredClone, хотя и является нативным для Node 17, несовместим со многими особенностями языка.
  • В других языках, таких как Java, каждый класс должен реализовывать свой метод клонирования, а в JavaScript пытаются написать “универсальный” метод клонирования. Вероятно, подход Java заслуживает большего внимания: вместо structuredClone предпочтительней иметь .clone в прототипе объекта, который можно переопределить в классе/объекте, если метод по умолчанию недостаточно хорош.
  • Универсальные решения плохи по определению. Они соблазняют своим удобством, но в итоге всегда терпят неудачу перед лицом возрастающей сложности.
  • Нельзя утверждать, но, вероятно, можно форкнуть cloneLib (npm install clone) и попытаться улучшить его, поскольку автор утверждает, что больше не поддерживает его.

Читайте также:

Читайте нас в TelegramVK и Дзен


Перевод статьи Tiago Bértolo: Which is the best method for deep cloning in Javascript? Are they all bad?

Предыдущая статьяНе заблудитесь при работе с кластерами Kafka  —  возьмите компас
Следующая статьяКак освоить API-интерфейсы Metal с UIView и SwiftUI