Глубокое копирование объектов в JavaScript: топ-5 способов

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: Почему обычное копирование не работает?

Представьте: вы пишете приложение, которое обрабатывает сложную конфигурацию пользователя. Вы делаете 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, циклических ссылок.
  • ✅ Работает с любыми типами.

Минусы:

  • ❌ Нужно писать и тестировать.
  • ❌ Может быть медленнее на очень больших объектах

Блоги

Рекомендаций по книгам