Javascript tiene una funcionalidad que al principio cuesta entender. Cuando tienes un objeto y lo igualas a otro, por ejemplo:

const hero = {
  name: "Batman",
  city: "Gotham",
};

const myHero = hero;

Si ahora cambias una de las propiedades del objeto se cambiará en los dos objetos. Esto es así porque realmente cuando pones el igual en los objetos, lo que estás haciendo es que el segundo objeto apunte a la misma dirección de memoria del primero:

const hero = {
  name: "Batman",
  city: "Gotham",
};

const myHero = hero;
myHero.name = "Spiderman";

console.log(myHero);
// { name: "Spiderman", city: "Gotham" } <-- ✅

console.log(hero);
// { name: "Spiderman", city: "Gotham" } <-- 😱

Cuando "clonas" un objeto de esta manera lo que estás haciendo es un "shallow copy". Si de verdad quieres clonarlo y que al modificar uno no se modifique el otro lo que tienes que hacer es deep copy.

La primera solución a esto es el spread operator. Por ejemplo:

const hero = {
  name: "Batman",
  city: "Gotham",
};

const myHero = { ...hero };
myHero.name = "Spiderman";

console.log(myHero);
// { name: "Spiderman", city: "Gotham" } <-- ✅

console.log(hero);
// { name: "Batman", city: "Gotham" } <-- ✅

Pero cuidadito porque esto tiene otro problema. Para objetos simples si que se hace deep copy, en cambio, para propiedades interas lo que hace es shallow copy.

const hero = {
  name: "Batman",
  city: "Gotham",
  contact: {
    mail: "batman@heroes.com",
  },
};

const myHero = { ...hero };
myHero.name = "Spiderman";
myHero.contact.mail = "spiderman@heroes.com";

console.log(myHero);
// { name: "Spiderman", city: "Gotham", contact: { mail: "spiderman@heroes.com" } } <-- ✅

console.log(hero);
// { name: "Batman", city: "Gotham", contact: { mail: "spiderman@heroes.com" } } <-- 😱

Deep Object.assign() de objetos

Con este snippet vas a poder crear una función que arregle los problemas del spread, es decir, que copie también de forma recursiva las propiedades internas de los objetos.

Ojo, si cambias el nombre a la función, tienes que recordar cambiarlo también en la llamada interna recursiva.

const merge = (target, source) => {
  // Miramos si la propiedad es también un objeto para copiarla de forma iterativa
  for (const key of Object.keys(source)) {
    if (source[key] instanceof Object)
      Object.assign(source[key], merge(target[key], source[key]));
  }
  // Combina el objeto resultante con el objeto de entrada
  Object.assign(target || {}, source);
  return target;
};

Forma alternativa si no necesitas copiar funciones

Otra forma rápida de copiar objetos complejos en Javascript es la siguiente:

const newObject = JSON.parse(JSON.stringify(oldObject));

Esta forma aunque es mucho más corta y simple de recordar tiene la desventaja de que no copia las funciones que tenga el objeto.

Ojo porque esta manera tiene un problema, y es que el stringify no tiene en cuenta los undefined y por tanto no se copian esas claves.

De forma nativa usando structuredClone

Por suerte en Javascript ya existe una forma de hacer deep copy de objetos, el problema es que todavía no está implementada.

La función se llama structuredClone, y aunque ya se puede usar en Firefox, en el resto de navegadores todavía no está implementada.

Puedes ver en la página de Can I use el soporte que tiene en los distintos navegadores.

Los de MDN también han creado una página con documentación sobre structuredClone

Usando Lodash

Si ya estás usando Lodash en el proyecto tienes que saber que ya viene con su propia función de cloneDeep. Lo bueno que tiene es que puedes pasar más de un objeto en un Array:

const objects = [{ a: 1 }, { b: 2 }];

const deep = _.cloneDeep(objects);

Puedes consultar documentación sobre este método en su página oficial.

En caso de que no tengas Lodash en el proyecto te recomiendo que no instales toda la librería solo para esa función. Existe esta otra librería que exporta únicamente la función cloneDeep de Lodash:

npm i --save lodash.clonedeep
const cloneDeep = require("lodash.clonedeep");