Avatar ·

Самый эффективный способ глубокого клонирования объекта в JavaScript

Всем привет! Я новичок в JavaScript и столкнулся с проблемой при копировании объектов. Мне нужно создать независимую копию объекта, чтобы изменения в копии не влияли на оригинал. Вот пример кода, который у меня не работает: ```javascript const original = { name: 'Анна', age: 25, address: { city: 'Москва', street: 'Ленина' }, hobbies: ['чтение', 'спорт'] }; const copy = original; copy.name = 'Мария'; copy.address.city = 'СПб'; console.log(original.name); // 'Мария' — изменилось! console.log(original.address.city); // 'СПб' — тоже изменилось! ``` Я попробовал использовать `Object.assign()` и spread-оператор: ```javascript const copy2 = Object.assign({}, original); const copy3 = {...original}; copy2.address.city = 'Казань'; console.log(original.address.city); // 'Казань' — опять изменилось! ``` Проблема в том, что вложенные объекты и массивы копируются по ссылке. Я слышал про `JSON.parse(JSON.stringify())`, но говорят, что это не работает для функций, undefined и специальных объектов (Date, RegExp). Подскажите, как правильно и эффективно сделать глубокое клонирование объекта в JavaScript? Какие есть современные методы? Что лучше использовать в реальных проектах? <<>>

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

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

1. JSON.parse/JSON.stringify — простой, но ограниченный

const deepClone = obj => JSON.parse(JSON.stringify(obj));

const original = {
  name: 'Анна',
  date: new Date(),
  func: () => console.log('test'),
  undef: undefined,
  regexp: /test/gi
};

const clone = deepClone(original);
console.log(clone.date);      // строка, не Date!
console.log(clone.func);      // undefined — функция потеряна
console.log(clone.undef);     // undefined — но в JSON нет undefined
console.log(clone.regexp);    // {} — RegExp превращается в пустой объект

Минусы: Не работает с функциями, undefined, Symbol, Date, RegExp, Map, Set, циклическими ссылками. Подходит только для простых JSON-совместимых данных.

2. structuredClone() — современный нативный метод (рекомендуется)

Начиная с 2022 года, в браузерах и Node.js 17+ доступна функция structuredClone():

const original = {
  name: 'Анна',
  date: new Date(),
  nested: { arr: [1, 2, { a: 1 }] },
  map: new Map([['key', 'value']]),
  set: new Set([1, 2, 3])
};

const clone = structuredClone(original);
clone.nested.arr[2].a = 999;
console.log(original.nested.arr[2].a); // 1 — не изменилось!
console.log(clone.date instanceof Date); // true — Date сохранился

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

  • Поддерживает Date, RegExp, Map, Set, ArrayBuffer, Blob, ImageData и другие
  • Работает с циклическими ссылками
  • Самый быстрый нативный метод
  • Безопасный — не выполняет произвольный код

Ограничения: Не копирует функции, Symbol, WeakMap, WeakSet, DOM-узлы.

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) return new Map([...obj].map(([k, v]) => [deepClone(k, hash), deepClone(v, hash)]));
  if (obj instanceof Set) return new Set([...obj].map(v => deepClone(v, hash)));
  
  // Обработка циклических ссылок
  if (hash.has(obj)) return hash.get(obj);
  
  const clone = Array.isArray(obj) ? [] : {};
  hash.set(obj, clone);
  
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      clone[key] = deepClone(obj[key], hash);
    }
  }
  
  // Копирование Symbol-ключей
  const symbols = Object.getOwnPropertySymbols(obj);
  for (const sym of symbols) {
    clone[sym] = deepClone(obj[sym], hash);
  }
  
  return clone;
}

// Пример с циклической ссылкой
const circular = { name: 'test' };
circular.self = circular;
const cloned = deepClone(circular);
console.log(cloned.self === cloned); // true — циклическая ссылка сохранена

Что выбрать в 2024 году?

  • Для 90% случаев: используйте structuredClone(). Это самый быстрый и надёжный способ для современных проектов
  • Для Node.js < 17 или старых браузеров: используйте библиотеку lodash.cloneDeep() — проверенное решение с хорошей поддержкой
  • Если нужны функции и Symbol: пишите свою рекурсивную реализацию или используйте rfdc (really fast deep clone) — очень быструю библиотеку

Производительность: structuredClone() в 2-10 раз быстрее JSON.parse/stringify для больших объектов и в 50+ раз быстрее рекурсивной реализации. Всегда тестируйте на своих данных.

<<>>

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

Если вы не можете использовать structuredClone() из-за поддержки старых браузеров или вам нужны дополнительные возможности, обратите внимание на проверенные библиотеки:

Lodash (самая популярная)

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

const original = { a: 1, b: { c: 2 }, d: [1, 2, 3] };
const clone = cloneDeep(original);
clone.b.c = 999;
console.log(original.b.c); // 2 — не изменилось

Плюсы: Работает с функциями, Symbol, циклическими ссылками. Проверена миллионами проектов.

Минусы: Размер библиотеки (~24 KB gzip), но можно импортировать только cloneDeep.

rfdc (Really Fast Deep Clone)

// Установка: npm i rfdc
const clone = require('rfdc')();

const fast = clone({ a: 1, b: { c: 2 } });

Плюсы: Один из самых быстрых клонов (написан на C++ через Node.js addon). Работает с Date, RegExp, Map, Set.

Минусы: Не копирует функции и Symbol.

Совет для начинающих

Начните с structuredClone() — это встроенная функция, не требующая установки библиотек. Если проект требует поддержки IE11 или старых версий Node.js, используйте lodash.cloneDeep. И помните: глубокое клонирование больших объектов может быть дорогим по памяти, поэтому старайтесь минимизировать его использование в критичных участках кода.