Как реализовать глубокое клонирование объекта в JavaScript?
Что проверяют
Знает ли кандидат разницу между поверхностной и глубокой копией, помнит ли ограничения каждого способа и назовёт ли современный штатный structuredClone вместо трюка с JSON.
Как отвечать
Первым называть structuredClone(obj): это стандарт, доступный в браузерах и в Node с 17 версии. Он корректно обрабатывает циклические ссылки, Date, RegExp, Map, Set, ArrayBuffer, типизированные массивы, Blob.
Его ограничения тоже надо знать: функции и символы вызывают DataCloneError, DOM-узлы не клонируются, геттеры превращаются в обычные значения, а прототип теряется, то есть экземпляр класса вернётся простым объектом.
JSON.parse(JSON.stringify(obj)) годится только для plain-данных, пришедших с сервера. Потери:
undefined, функции и символы просто исчезают;Dateстановится строкой;NaNиInfinityпревращаются вnull;MapиSetсериализуются в{};BigIntбросает ошибку, циклическая ссылка тоже.
Своя рекурсия нужна, когда требуется контроль над типами:
function deepClone(value, seen = new WeakMap()) {
if (value === null || typeof value !== "object") return value;
if (seen.has(value)) return seen.get(value);
if (value instanceof Date) return new Date(value);
if (value instanceof RegExp) return new RegExp(value.source, value.flags);
const copy = Array.isArray(value)
? []
: Object.create(Object.getPrototypeOf(value));
seen.set(value, copy);
if (value instanceof Map) {
value.forEach((v, k) => copy.set(deepClone(k, seen), deepClone(v, seen)));
return copy;
}
for (const [k, v] of Object.entries(value)) copy[k] = deepClone(v, seen);
return copy;
}Здесь важны две детали. WeakMap закрывает циклические ссылки: уже виденный объект возвращается из кеша, а не клонируется заново. И обход идёт через Object.entries, а не for...in: последний поднимается по цепочке прототипов и тащит унаследованные свойства.
Если lodash уже в проекте, _.cloneDeep покрывает больше типов и сохраняет прототипы, изобретать своё не нужно.
Полезное дополнение к ответу: глубокая копия нужна реже, чем кажется. Для иммутабельного обновления состояния достаточно копировать только изменяемую ветку ({ ...obj, nested: { ...obj.nested, a: 1 } }), а глубокое клонирование больших структур на каждый рендер бьёт по производительности.
Где спотыкаются
Называют JSON-трюк единственным вариантом и не помнят его потерь. На вопрос «что будет с полем типа Date» отвечают «останется датой», хотя вернётся строка.