Avatar ·

Как правильно клонировать объект в JavaScript? Проблемы с Object.assign и spread оператором

Всем привет! Я новичок в JavaScript и столкнулся с проблемой при копировании объектов. У меня есть объект `user` с вложенными данными: ```javascript const user = { name: 'Иван', age: 30, address: { city: 'Москва', street: 'Тверская', coordinates: { lat: 55.76, lng: 37.62 } }, hobbies: ['программирование', 'шахматы'] }; ``` Я пытаюсь создать копию этого объекта, но столкнулся с неожиданным поведением. Когда я использую `Object.assign` или spread оператор, изменения во вложенных объектах влияют на исходный объект: ```javascript const clone1 = Object.assign({}, user); const clone2 = { ...user }; clone1.address.city = 'Санкт-Петербург'; console.log(user.address.city); // 'Санкт-Петербург' — исходный объект тоже изменился! clone2.hobbies.push('чтение'); console.log(user.hobbies); // ['программирование', 'шахматы', 'чтение'] — опять изменился! ``` Я читал, что это называется "поверхностное копирование". Как сделать глубокое копирование, чтобы вложенные объекты и массивы тоже копировались независимо? Какие есть правильные способы клонирования объектов в JavaScript? Нужно ли использовать сторонние библиотеки или есть встроенные методы? Заранее спасибо за помощь! <<>>

Проблема поверхностного копирования

Вы правы — Object.assign() и spread оператор (...) создают только поверхностную копию. Это значит, что вложенные объекты и массивы копируются по ссылке, а не по значению. Для независимой копии нужно использовать глубокое (deep) клонирование.

1. Лучший современный способ: structuredClone()

Начиная с ES2021 (и доступно в Node.js 17+, Chrome 98+, Firefox 94+), существует встроенная функция structuredClone(). Это самый правильный и безопасный способ:

const user = {
  name: 'Иван',
  age: 30,
  address: { city: 'Москва', street: 'Тверская' },
  hobbies: ['программирование', 'шахматы'],
  date: new Date() // structuredClone корректно копирует даты
};

const deepClone = structuredClone(user);

// Теперь изменения не влияют на оригинал
deepClone.address.city = 'Санкт-Петербург';
deepClone.hobbies.push('чтение');

console.log(user.address.city);      // 'Москва' — оригинал не изменился
console.log(user.hobbies.length);    // 2 — оригинал не изменился
console.log(deepClone.address.city); // 'Санкт-Петербург'

2. Классический способ: JSON.parse/JSON.stringify

Этот метод работает везде, но имеет ограничения:

const jsonClone = JSON.parse(JSON.stringify(user));

jsonClone.address.city = 'Казань';
console.log(user.address.city); // 'Москва' — работает

Ограничения метода:

  • Не копирует функции, undefined, Symbol
  • Некорректно обрабатывает Date (превращает в строку), Set, Map, RegExp
  • Не работает с циклическими ссылками

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 (hash.has(obj)) return hash.get(obj); // защита от циклических ссылок

  const clone = Array.isArray(obj) ? [] : {};
  hash.set(obj, clone);

  for (const key of Object.keys(obj)) {
    clone[key] = deepClone(obj[key], hash);
  }
  return clone;
}

// Пример с циклической ссылкой
const cyclicObj = { name: 'Цикл' };
cyclicObj.self = cyclicObj;

const safeClone = deepClone(cyclicObj);
console.log(safeClone.self === safeClone); // true — циклическая ссылка сохранена

4. Сторонние библиотеки (если нужно в старых браузерах)

  • Lodash: _.cloneDeep(obj) — самый популярный вариант
  • Ramda: R.clone(obj)

Что выбрать?

  • Всегда используйте structuredClone() — если ваша среда выполнения его поддерживает (проверьте через typeof structuredClone === 'function')
  • JSON.parse/JSON.stringify — для простых объектов без специальных типов
  • Рекурсивная функция — когда нужен полный контроль или поддержка циклических ссылок
  • Lodash — если вы уже используете эту библиотеку в проекте

Избегайте поверхностного копирования с Object.assign или spread, если объект содержит вложенные структуры.

<<>>

Альтернативный подход: библиотека Lodash

Если вы не можете использовать structuredClone (например, работаете со старыми браузерами или Node.js версии ниже 17), отличным решением будет библиотека Lodash и её метод _.cloneDeep().

// Установка: npm install lodash
import _ from 'lodash';

const user = {
  name: 'Иван',
  address: { city: 'Москва' },
  hobbies: ['программирование'],
  birthDate: new Date('1993-01-01'),
  skills: new Set(['JS', 'React']),
  meta: undefined
};

const cloned = _.cloneDeep(user);

cloned.address.city = 'Санкт-Петербург';
cloned.hobbies.push('чтение');

console.log(user.address.city); // 'Москва' — оригинал сохранен
console.log(cloned.birthDate instanceof Date); // true — дата скопирована корректно

Преимущества Lodash:

  • Работает во всех средах (включая IE11)
  • Поддерживает Date, RegExp, Set, Map, ArrayBuffer и другие типы
  • Корректно обрабатывает циклические ссылки
  • Хорошо оптимизирован для больших объектов

Недостаток: добавляет зависимость (~4 КБ в сжатом виде, если использовать только этот метод через lodash.clonedeep).

Если проект уже использует Lodash — это идеальный выбор. Для новых проектов рекомендую structuredClone, так как он встроенный и не требует дополнительных зависимостей.