Перейти к содержимому

Deep Clone

Глубокое клонирование — частый вопрос; слабая реализация через JSON.parse(JSON.stringify(…)) теряет Date, RegExp, Map, Set и циклические ссылки. Интервьюер проверяет знание WeakMap для обнаружения циклов, корректную обработку специальных типов и осведомлённость о встроенном structuredClone.

Рекурсивный deepClone с обработкой циклов, Date, RegExp, Map, Set, Symbol-ключей:

/**
* Глубокое клонирование: plain objects, arrays, Date, RegExp, Map, Set, circular refs.
* @template T
* @param {T} value
* @param {WeakMap} [seen] — для отслеживания циклических ссылок
* @returns {T}
*/
function deepClone(value, seen = new WeakMap()) {
// Примитивы, null, функции — возвращаем as-is (функции не клонируются)
if (value === null || typeof value !== 'object') return value;
// Циклическая ссылка → вернуть уже созданную копию
if (seen.has(value)) return seen.get(value);
// Date
if (value instanceof Date) return new Date(value.getTime());
// RegExp — cloneFlags через value.flags (ES2015+)
if (value instanceof RegExp) return new RegExp(value.source, value.flags);
// Map — клонируем ключи и значения рекурсивно
if (value instanceof Map) {
const clone = new Map();
seen.set(value, clone);
for (const [k, v] of value) clone.set(deepClone(k, seen), deepClone(v, seen));
return clone;
}
// Set
if (value instanceof Set) {
const clone = new Set();
seen.set(value, clone);
for (const v of value) clone.add(deepClone(v, seen));
return clone;
}
// Array
if (Array.isArray(value)) {
const clone = new Array(value.length);
seen.set(value, clone);
for (let i = 0; i < value.length; i++) clone[i] = deepClone(value[i], seen);
return clone;
}
// Plain object: сохраняем прототип, клонируем Symbol-ключи
const clone = Object.create(Object.getPrototypeOf(value));
seen.set(value, clone);
for (const key of Reflect.ownKeys(value)) {
const desc = Object.getOwnPropertyDescriptor(value, key);
Object.defineProperty(clone, key, {
...desc,
value: deepClone(desc.value, seen),
});
}
return clone;
}

Тесты: цикл, Date, Map, Set, вложенные массивы:

// ─── Тест корректности ───────────────────────────────────────────────────
const src = {
name: 'test',
date: new Date('2024-01-01'),
rx: /hello/gi,
map: new Map([['key', { nested: 1 }]]),
set: new Set([1, 2, { deep: true }]),
arr: [1, [2, [3]]],
};
src.self = src; // циклическая ссылка
const copy = deepClone(src);
console.assert(copy.name === 'test');
console.assert(copy.date.getTime() === src.date.getTime());
console.assert(copy.date !== src.date); // разные объекты Date
console.assert(copy.rx.source === 'hello');
console.assert(copy.rx !== src.rx); // разные RegExp
console.assert(copy.map.get('key').nested === 1);
console.assert(copy.map !== src.map); // глубокая копия Map
console.assert(copy.self === copy); // цикл разрешён
console.assert(copy.self !== src); // цикл ведёт на копию, не оригинал
console.assert(copy.arr[1][1][0] === 3);
// Изменение копии не влияет на оригинал
copy.map.get('key').nested = 99;
console.assert(src.map.get('key').nested === 1); // оригинал не тронут

Встроенный structuredClone — предпочтительный вариант в продакшне:

// structuredClone (Node.js 17+, Chrome 98+, Firefox 94+, Safari 15.4+)
// Обрабатывает: Date, Map, Set, ArrayBuffer, TypedArray, Error, циклы
const obj = {
date: new Date(),
map: new Map([['a', 1]]),
arr: [1, [2, [3]]],
};
obj.circular = obj;
const clone = structuredClone(obj);
clone.date.setFullYear(2000); // не влияет на оригинал
console.assert(obj.date.getFullYear() !== 2000);
// ─── Что structuredClone НЕ поддерживает ────────────────────────────────
try {
structuredClone({ fn: () => {} }); // DataCloneError: функции нельзя клонировать
} catch (e) { console.error(e.name); }
// Symbol-ключи — молча игнорируются (нет ошибки, просто пропускаются)
const sym = Symbol('key');
const withSym = { [sym]: 42, plain: 1 };
const cloned = structuredClone(withSym);
console.assert(cloned[sym] === undefined); // Symbol-ключ потерян!
// Кастомные классы теряют прототип:
class Point { constructor(x, y) { this.x = x; this.y = y; } }
const p = new Point(1, 2);
const pc = structuredClone(p);
console.assert(!(pc instanceof Point)); // стал plain object
// Вывод: structuredClone в продакшне.
// Ручная реализация с WeakMap — на интервью.

Сложность

  • Время: O(n) где n = суммарное количество узлов во всём графе объекта
  • Память: O(n) для клонированных значений + O(d) стек рекурсии, d = глубина

Итог: Корректный deepClone: WeakMap для циклов, явная обработка Date/RegExp/Map/Set, Symbol-ключи через Reflect.ownKeys. В продакшне — structuredClone.