Введение: Почему обычное копирование не работает?
Представьте: вы пишете приложение, которое обрабатывает сложную конфигурацию пользователя. Вы делаете const newConfig = oldConfig, меняете пару полей — и вдруг оригинальный объект тоже меняется. Знакомая боль? 👋
В JavaScript объекты передаются по ссылке. Это значит, что простая операция присваивания не создаёт новый объект, а лишь копирует указатель на старый. Отсюда и берётся главный вопрос, который мучает тысячи разработчиков на Stack Overflow: "What is the most efficient way to deep clone an object in JavaScript?".
В этой статье мы разберём все популярные способы глубокого копирования, их плюсы, минусы и производительность. Вы узнаете, какой метод выбрать для 2024 года, и научитесь писать собственный надёжный deep clone.
Почему мы вообще говорим о глубоком копировании?
Глубокое копирование (deep clone) создаёт полностью независимую копию объекта со всеми вложенными структурами. В отличие от поверхностного (shallow copy), где вложенные объекты остаются общими.
Вот типичный сценарий, когда это нужно:
- Работа с Redux / Zustand: нельзя мутировать состояние, нужно создавать копии.
- Сериализация данных для отправки на сервер.
- Работа с кэшем или конфигурациями, которые нельзя менять.
- Unit-тесты: чтобы не повлиять на исходные данные.
Но не всё так просто. Если вы попробуете скопировать объект с функциями, undefined, Symbol или циклическими ссылками — многие методы упадут или потеряют данные. Давайте разберём каждый способ детально.
Метод 1: JSON.parse(JSON.stringify(obj)) — старый, но рабочий
Это самый популярный способ среди новичков. Он простой и быстрый в написании:
const original = {
name: "Анна",
age: 25,
address: { city: "Москва", zip: 101000 },
hobbies: ["чтение", "бег"]
};
const clone = JSON.parse(JSON.stringify(original));
clone.address.city = "Санкт-Петербург";
console.log(original.address.city); // "Москва" — ✅ копия независима!
Плюсы:
- 🔥 Очень простой синтаксис.
- 🔥 Работает для большинства простых случаев.
Минусы (критичные!):
- ❌ Теряет функции — они превратятся в
undefined. - ❌ Теряет undefined — поля со значением
undefinedисчезнут. - ❌ Теряет Symbol — ключи-символы пропадут.
- ❌ Не работает с циклическими ссылками — выбросит ошибку
TypeError. - ❌ Проблемы с датами —
Dateпревратится в строку. - ❌ Проблемы с Map, Set, RegExp — они станут пустыми объектами.
Вывод: Подходит только для данных, которые гарантированно сериализуемы в JSON (строки, числа, массивы, простые объекты).
Метод 2: Structured Clone API — современный стандарт (2024)
Начиная с 2022 года, в JavaScript появился нативный способ глубокого копирования — structuredClone. Он доступен во всех современных браузерах и Node.js 17+.
const clone = structuredClone(original);
clone.address.city = "Казань";
console.log(original.address.city); // "Москва" — ✅
Что может structuredClone:
- ✅ Циклические ссылки — работает!
- ✅ Даты — сохраняет тип
Date. - ✅ Map и Set — корректно копирует.
- ✅ RegExp — тоже работает.
- ✅ ArrayBuffer и TypedArray — поддерживаются.
Чего он не умеет:
- ❌ Функции — выбросит
DataCloneError. - ❌ Symbol — не поддерживается.
- ❌ WeakMap, WeakSet — не копируются.
// Пример с циклической ссылкой
const cyclicObj = { name: "Цикл" };
cyclicObj.self = cyclicObj;
const clone = structuredClone(cyclicObj);
console.log(clone.self === clone); // true — ✅ круг не сломан!
Вывод: Это лучший способ по умолчанию для 90% задач. Быстрее JSON.parse, безопаснее, поддерживает больше типов. Используйте его, если не нужно копировать функции.
Метод 3: Рекурсивная функция — полный контроль
Если вам нужно копировать абсолютно всё (включая функции и символы), придётся писать свою реализацию. Это не так страшно, как кажется:
function deepClone(obj, hash = new WeakMap()) {
if (obj === null || typeof obj !== 'object') return obj;
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj);
if (obj instanceof Map) {
const cloneMap = new Map();
obj.forEach((value, key) => cloneMap.set(deepClone(key), deepClone(value)));
return cloneMap;
}
if (obj instanceof Set) {
const cloneSet = new Set();
obj.forEach(value => cloneSet.add(deepClone(value)));
return cloneSet;
}
// Обработка циклических ссылок
if (hash.has(obj)) return hash.get(obj);
const clone = Array.isArray(obj) ? [] : {};
hash.set(obj, clone);
// Копируем все ключи, включая символы
for (const key of Reflect.ownKeys(obj)) {
clone[key] = deepClone(obj[key], hash);
}
return clone;
}
// Пример с функциями и Symbol
const objWithFunc = {
greet: () => console.log("Привет!"),
[Symbol('id')]: 123
};
const clone = deepClone(objWithFunc);
console.log(typeof clone.greet); // "function" — ✅
console.log(clone.greet === objWithFunc.greet); // false — новая ссылка
Плюсы:
- ✅ Полный контроль над логикой.
- ✅ Поддержка функций, Symbol, циклических ссылок.
- ✅ Работает с любыми типами.
Минусы:
- ❌ Нужно писать и тестировать.
- ❌ Может быть медленнее на очень больших объектах