Aprende Correctamente a Clonar Objetos en JavaScript
Debido a que los objetos en JavaScript son pasados por referencia, no puedes simplemente copiar usando el =
.
Pero no te preocupes, hoy aprenderemos 3 formas en que puedes clonar un objeto 😁
Los objetos son pasados por referencia
Tu primera pregunta podría ser, ¿por qué no puedo usar =
?.
Veamos qué pasa si hacemos eso:
const objeto = { uno: 1, dos: 2 };const objeto2 = objeto;console.log( objeto, // { uno: 1, dos: 2 }; objeto2, // { uno: 1, dos: 2 };);
Hasta ahora, ambos objetos parecen dar la misma salida. Así que no hay problema, ¿verdad? Pero veamos qué pasa si editamos nuestro segundo objeto:
const objeto2.tres = 3;console.log(objeto2);// { uno: 1, dos: 2, tres: 3 }; <-- ✅console.log(objeto);// { uno: 1, dos: 2, tres: 3 }; <-- 😱
¡¿QUÉ?! Cambié objeto2
pero ¿por qué también se vio afectado objeto
? 🤔
Eso es porque los objetos son pasados por referencia. Así que cuando usas =
, se copia el puntero al espacio de memoria que ocupa.
Las referencias no contienen valores, son un puntero al valor en memoria.
Si quieres aprender más sobre esto, puedes revisar este artículo donde se da una explicación más detallada sobre ello.
1. Usando el operador Spread
Usando el operador spread se clonará tu objeto. Ten en cuenta que será una copia superficial.
const comida = { carne: '🥩', tocino: '🥓' };const comidaClonada = { ...comida };console.log(comidaClonada);// { carne: '🥩', tocino: '🥓' }
2. Usando Object.assign
Podemos usar Object.assign
que también creará una copia superficial del objeto.
const comida = { carne: '🥩', tocino: '🥓' };const comidaClonada = Object.assign({}, comida);console.log(comidaClonada);// { carne: '🥩', tocino: '🥓' }
Nota el objeto vacío {}
como primer argumento, esto asegurará que no mutes el objeto original 👍
3. Usando JSON
Esta forma es comúnmente muy recomendada ya que hará una copia profunda. Ahora, esta es una forma rápida y sucia de clonar un objeto en profundidad.
const comida = { carne: '🥩', tocino: '🥓' };const comidaClonada = JSON.parse(JSON.stringify(comida));console.log(comidaClonada);// { carne: '🥩', tocino: '🥓' }
Esto funciona, pero tiene sus propios inconvenientes: sólo se clonan correctamente los tipos JSON válidos. Para una solución más robusta, yo recomendaría usar algo como lodash.
Lodash DeepClone vs JSON
Hay que tener en cuenta que hay algunas diferencias entre deepClone y JSON.stringify/parse.
- JSON.stringify/parse sólo funciona con
Number
,String
yObject
literales sin propiedades de función o Símbolo. - deepClone trabaja con todos los tipos, la función y el Symbol se copian por referencia.
Aquí hay un ejemplo:
const lodashClonedeep = require('lodash.clonedeep');const array = [ () => 2, { prueba: () => 3, }, Symbol('4'),];// deepClone copia por referencia la función y el Symbolconsole.log(lodashClonedeep(array));// [() => 2, { prueba: () => 3, }, Symbol('4')]// JSON reemplaza la función y el Symbol por null y la función en el objeto por undefinedconsole.log(JSON.parse(JSON.stringify(array)));// [ null, {}, null ]// la función y el Symbol se copian por referencia en deepCloneconsole.log( lodashClonedeep(array)[0] === lodashClonedeep(array)[0],); // trueconsole.log( lodashClonedeep(array)[2] === lodashClonedeep(array)[2],); // true
Además, copiar usando JSON tiene problemas con las dependencias circulares y el orden de las propiedades en el objeto clonado puede ser diferente, pero si no estás usando nada más que JSON válido, este podría ser el enfoque más simple.
Clon superficial vs clon profundo
Cuando se utilizada spread ...
para copiar un objeto, sólo estamos creando una copia superficial. Si el array está anidado o es multidimensional, no funcionará. Este es el ejemplo que vamos a utilizar:
const objetoAnidado = { bandera: '🇵🇪', pais: { ciudad: 'lima', },};
Copia superficial
Vamos a clonar nuestro objeto usando spread:
const clonSuperficial = { ...objetoAnidado };// Changed our cloned objectclonSuperficial.bandera = '🇺🇸';clonSuperficial.pais.ciudad = 'washington';
Así que cambiamos nuestro objeto clonado cambiando la ciudad. Veamos el resultado.
console.log(clonSuperficial);// {bandera: '🇺🇸', pais: {ciudad: 'washington'}}console.log(objetoAnidado);// {bandera: '🇵🇪', pais: {ciudad: 'washington'}} <-- 😱
Una copia superficial significa que se copia el primer nivel, los niveles más profundos se referencian.
Copia profunda
Tomemos el mismo ejemplo pero aplicando una copia profunda utilizando JSON
const clonSuperficial = JSON.parse(JSON.stringify(objetoAnidado));clonSuperficial.bandera = '🇺🇸';clonSuperficial.pais.ciudad = 'washington';console.log(clonSuperficial);// {bandera: '🇺🇸', pais: {ciudad: 'washington'}}console.log(objetoAnidado);// {bandera: '🇵🇪', pais: {ciudad: 'lima'}} <-- ✅
Como puedes ver, la copia profunda es una verdadera copia para los objetos anidados. Muchas veces la copia superficial es lo suficientemente buena, no se necesita realmente una copia profunda. Es como una pistola de clavos frente a un martillo. La mayoría de las veces el martillo está perfectamente bien. El uso de una pistola de clavos para algunas pequeñas artesanías es a menudo un caso de exageración, un martillo está muy bien. Se trata de utilizar la herramienta adecuada para el trabajo adecuado 🤓
Rendimiento
Probando el operador Spread vs Object.assign vs Lodash cloneDeep vs JSON, el resultado muestra que el operador Spread es mucho más rápido que todos los demás 🥇, pero ten en cuenta que solo hace una copia superficial! Viendo el lado de la copia profunda (Lodash vs JSON), Lodash tiene mejor performance 💯
Otros Recursos
- Puedes revisar este script que creó Chris Ferdinandi para clonar un objeto o array con vanilla JS, es muy parecido a lo que hace JSON pero con mejor rendimiento. Puedes ver la demo aquí.