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); // разные объекты Dateconsole.assert(copy.rx.source === 'hello');console.assert(copy.rx !== src.rx); // разные RegExpconsole.assert(copy.map.get('key').nested === 1);console.assert(copy.map !== src.map); // глубокая копия Mapconsole.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.