В данном исследовании сравниваются различные методы глубокого клонирования в 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
.

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

❌ = выбранный метод не смог клонировать данный тип.
✅ = выбранный метод успешно клонировал данный тип.
Худший метод
Наименее эффективным методом клонирования оказался простой cloneJSON
.
Этот метод состоит из простого JSON.parse после JSON.stringify. К сожалению, он широко используется.
# реализация cloneJSON
function cloneJSON(obj) {
return JSON.parse(JSON.stringify(obj))
}
cloneJSON
— очень медленный метод с ограниченными возможностями. Поэтому стоит избегать его.
Неожиданность
structuredClone
не может клонировать данные типа Symbol
. Он выбрасывает DOMException с кодом 25, DataCloneError
.
Выбрасывание ошибки кажется несколько излишним. Почему бы просто не скопировать ссылку, ведь данные типа Symbol
уникальны и неизменяемы?
Этот метод появился сравнительно недавно: впервые он был реализован в Node 17. Проблемы с ним на этом не заканчиваются.

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

Сравним результаты каждого метода клонирования для каждого из четырех заголовков:
- basic (базовый);
- property descriptors (дескрипторы свойств);
- object state (состояние объекта);
- prototype (прототип).
Результаты тестирования: базовый

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

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

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

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

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

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

Только три библиотеки могут правильно клонировать прототип.
Были использованы два метода:
structuredClone
копировал прототип в новый независимый объект;- клоны
cloneDeep
иcloneLib
ссылаются на тот же объект, что и во входных данных.
Оба подхода можно считать корректными, но предпочтительней, на наш взгляд, structuredClone
.
Хотя это ужасная практика, некоторые библиотеки меняют прототип во время выполнения, поэтому предпочтительнее ссылаться на входной прототип вместо реального клонирования.
Примечательно, что cloneDeep
не может скопировать неперечислимые свойства в обычном объекте, но может сделать это внутри прототипа.

Третий тест снова показывает, что 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
.

Этот метод производит связанную функцию, которая является просто оберткой исходной функции.
Данный метод основан на этом ответе на StackOverflow. Ниже можете увидеть “foo” и его клон, созданный этим методом:


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

Победителем стал cloneLib
(npm install clone).
Итоги
- В JavaScript нет ни одной полностью работоспособной библиотеки и метода глубокого клонирования.
cloneLib
(npm install clone) — лучший метод.cloneJSON
не следует использовать.structuredClone
, хотя и является нативным для Node 17, несовместим со многими особенностями языка.- В других языках, таких как Java, каждый класс должен реализовывать свой метод клонирования, а в JavaScript пытаются написать “универсальный” метод клонирования. Вероятно, подход Java заслуживает большего внимания: вместо
structuredClone
предпочтительней иметь.clone
в прототипе объекта, который можно переопределить в классе/объекте, если метод по умолчанию недостаточно хорош. - Универсальные решения плохи по определению. Они соблазняют своим удобством, но в итоге всегда терпят неудачу перед лицом возрастающей сложности.
- Нельзя утверждать, но, вероятно, можно форкнуть
cloneLib
(npm install clone) и попытаться улучшить его, поскольку автор утверждает, что больше не поддерживает его.
Читайте также:
- 10 лайфхаков JavaScript, которые сделают из вас профессионала
- 4 функциональные концепции, которые следует знать каждому разработчику JavaScript
- Управление памятью JavaScript: как избежать утечек памяти и повысить производительность
Читайте нас в Telegram, VK и Дзен
Перевод статьи Tiago Bértolo: Which is the best method for deep cloning in Javascript? Are they all bad?