В JavaScript у нас может быть два типа переменных: примитивные типы и ссылочные типы.

Переменная примитивного типа — это данные, которые не являются объектом и не имеют методов. Последний стандарт ECMAScript определяет 7 примитивных типов: число, строка, логическое значение, неопределенный, нулевой, символ, большой. Когда мы создаем переменную примитивного типа, этой переменной напрямую присваивается значение.

Ссылочные типы работают по-разному. Когда мы создаем переменные ссылочного типа: объекты, значение этой переменной не присваивается напрямую, вместо этого ей присваивается ссылка (местоположение в памяти) значения.

Другими словами, значение примитивного типа является фактическим значением. Значение ссылочного типа является ссылкой на другое значение.

Итак, как это влияет на клонирование?

Если мы попытаемся скопировать переменную ссылочного типа, назначив ее другой переменной, мы в конечном итоге создадим еще одну ссылку на тот же объект. Это означает, что изменение оригинала также повлияет на клонированные версии и наоборот.

Итак, как можно решить эту проблему?

Одним из самых простых решений было бы создать новый объект и назначить свойства отдельно. Что мы можем сделать с ES6+, так это использовать оператор распространения.

const starWarsCharacter = { name: 'Han Solo', species: 'Human' };
const otherStarWarsCharacter = { ...starWarsCharacter };
otherStarWarsCharacter.name = 'Chewbacca';
otherStarWarsCharacter.species = 'Wookiee';
console.log(starWarsCharacter.name); // Han Solo
console.log(otherStarWarsCharacter.species); // Wookiee

На первый взгляд, такой подход решает нашу проблему. Но действительно ли это решает проблему?

No.

Если мы попытаемся клонировать объект, который не так прост, как этот, и имеет вложенные объекты внутри, мы закончим клонированием ссылок на эти вложенные объекты.

Чтобы решить и эту проблему, мы должны динамически перебирать все свойства и проверять, является ли свойство примитивным или ссылочным типом. Если это примитивный тип, то мы можем просто клонировать его, а если это ссылочный тип, мы должны использовать рекурсию для перебора его свойств, пока не будут учтены все вложенные примитивные свойства.

function cloneDeep(entity) {
  return /Array|Object/.test(Object.prototype.toString.call(entity))
    ? Object.assign(new entity.constructor, ...Object.keys(entity).map((prop) => ({ [prop]: cloneDeep(entity[prop]) })))
    : entity;
}
const obj1 = { a: 1, b: { c: 2, d: [ 3, 4, { e: 5 } ] } };
const obj2 = cloneDeep(obj1);
obj2.b.d[2].e = 6;
console.log(obj1.b.d[2].e); // 5
console.log(obj2.b.d[2].e); // 6

Вроде бы проблема с вложенными объектами решена, но это еще не окончательное решение. Есть случай, когда это выдаст ошибку. Это когда у нас есть циклические ссылки.

const obj1 = {};
obj1.ref = obj1;
const obj2 = cloneDeep(obj1); // Uncaught RangeError: Maximum call stack size exceeded

Как мы можем действовать сейчас? Чтобы решить эту ошибку, мы также должны изменить нашу функцию. Мы будем использовать структуру данных WeakMap для кэширования ссылок.

function cloneDeep(entity, cache = new WeakMap) {
  const referenceTypes = ['Array', 'Object'];
  const entityType = Object.prototype.toString.call(entity);
  if (!new RegExp(referenceTypes.join('|')).test(entityType)) return entity;
  if (cache.has(entity)) {
    return cache.get(entity);
  }
  const c = new entity.constructor;
  cache.set(entity, c);
  return Object.assign(c, ...Object.keys(entity).map((prop) => ({ [prop]: cloneDeep(entity[prop], cache) })));
}

С помощью этой функции мы решили проблему с циклическими ссылками. Но что, если у нас есть более сложные структуры данных. Давайте изменим нашу функцию, чтобы она также могла поддерживать Map, Set, Date. Итак, наша финальная функция будет выглядеть так.

function cloneDeep(entity, cache = new WeakMap) {
  const referenceTypes = ['Array', 'Object', 'Map', 'Set', 'Date'];
  const entityType = Object.prototype.toString.call(entity);
  if (
    !new RegExp(referenceTypes.join('|')).test(entityType) ||
    entity instanceof WeakMap ||
    entity instanceof WeakSet
  ) return entity;
  if (cache.has(entity)) {
    return cache.get(entity);
  }
  const c = new entity.constructor;
  
  if (entity instanceof Map) {
    entity.forEach((value, key) => c.set(cloneDeep(key), cloneDeep(value)));
  }
  if (entity instanceof Set) {
    entity.forEach((value) => c.add(cloneDeep(value)));
  }
  if (entity instanceof Date) {
    return new Date(entity);
  }
  cache.set(entity, c);
  return Object.assign(c, ...Object.keys(entity).map((prop) => ({ [prop]: cloneDeep(entity[prop], cache) })));
}

Вывод

При копировании переменной ссылочного типа на самом деле не копируется значение переменной, а копируется адрес, по которому хранится значение. Чтобы скопировать значение переменной ссылочного типа, мы должны перебрать все свойства и скопировать их по отдельности.

Вы можете использовать эту функцию для глубокого клонирования или создать свою собственную или использовать некоторые хорошо протестированные служебные библиотеки, в которые встроена эта функция.