Clonar objetos en Javascript (y en otros lenguajes) es una tarea complicada. JS no almacena el valor del objeto en tu variable o en tu constante; en su lugar, almacena un puntero al valor del objeto (la referencia del objeto).
Incluso cuando pasas un objeto a una función o método, estás pasando este objeto por referencia, no por valor.
Si pasas (o copias) un objeto por referencia y luego cambias cualquier propiedad, la propiedad del objeto ‘source’ también cambia.
En cualquier ejemplo, usaré el objeto de abajo:
const sourceObject = {
l1_1: {
l2_1: 123,
l2_2: [1, 2, 3, 4],
l2_3: {
l3_1: 'l3_3',
l3_3: () => 'l3_3',
},
},
l1_2: 'My original object',
};
Clonación ‘Standard’
Usaremos una clonación ‘standard’ asignando el valor de origen a otra constante:
const copiedObject = sourceObject;
console.log('sourceObject', sourceObject.l1_2);
// My original object --> ✔️
clonedObject.l1_2 = 'My cloned object';
console.log('clonedObject', clonedObject.l1_2);
// My original object --> ✔️
console.log('sourceObject', sourceObject.l1_2);
// My original object --> ❌
Como dije antes, cuando cambio la propiedad l1_2 en el objeto clonado, el valor también cambia en el objeto source.
Usando esta estrategia, no estás copiando el objeto en absoluto.
Usando spread operator
Esta vez usaré el spread operator, que ‘devuelve’ cada elemento del objeto individualmente.
console.log('sourceObject l1_2', sourceObject.l1_2);
// My original object --> ✔️
console.log('sourceObject l1_1.l2_1', sourceObject.l1_1.l2_1);
// 123 --> ✔️
const clonedObject = { ...sourceObject };
clonedObject.l1_2 = 'My cloned object';
console.log('clonedObject', clonedObject.l1_2);
// My cloned object --> ✔️
console.log('sourceObject', sourceObject.l1_2);
// My original object --> ✔️
clonedObject.l1_1.l2_1 = '321';
console.log('clonedObject l1_1.l2_1', clonedObject.l1_1.l2_1);
// 321 --> ✔️
console.log('sourceObject l1_1.l2_1', sourceObject.l1_1.l2_1);
// 321 --> ❌️ // Should keep returning 123 if the clone was complete
Ahora la propiedad l2_1 se copia por valor, podemos cambiarla y el l2_1 del objeto original mantiene su valor original, pero cuando cambié l1_1.l2_1 (propiedad de segundo nivel de profundidad) obtenemos lo mismo que en el primer intento.
El spread operator realiza una shallow copy del objeto. Solo las propiedades del primer nivel de profundidad se copian por valor, las anidadas se siguen copiando por referencia.
Usando Object.assign
Al igual que el spread operator, realiza una shallow copy, por lo que no crearé el ejemplo, créeme, obtendrás el mismo resultado.
const clonedObject = Object.assign({}, sourceObject);
Usando JSON.parse y JSON.stringify
Esta es una forma sencilla y rápida de realizar una deep clone de un objeto, el punto es convertir el objeto en un string con JSON.stringify y luego obtener un objeto del string usando JSON.parse.
Hagámoslo:
const clonedObject = JSON.parse(JSON.stringify(sourceObject));
clonedObject.l1_1.l2_1 = '321';
console.log('clonedObject l1_1.l2_1', clonedObject.l1_1.l2_1);
// 321 --> ✔️
console.log('sourceObject l1_1.l2_1', sourceObject.l1_1.l2_1);
// 123 --> ✔
¡Todo parece estar bien! 🎉
Pero, ¿notaste que la propiedad l1_1.l2_3.l3_3 es una función? 😢
console.log('clonedObject l1_1.l2_3.l3_3', clonedObject.l1_1.l2_3.l3_3);
// undefined --> ❌️
console.log('sourceObject l1_1.l2_3.l3_3', sourceObject.l1_1.l2_3.l3_3);
// function l3_3() {} --> ✔️
Oh, oh, las funciones no se copian usando ese método, entonces, ¿qué podríamos hacer? La solución es iterar cada propiedad anidada en el objeto y usar, por ejemplo, el método del spread operator. Es un trabajo duro y sucio.
Lodash al rescate
Lodash es una librería de utilidades modular que añade muchas funcionalidades, y una de ellas es cloneDeep, que hace exactamente lo que necesitamos para clonar (profundamente) un objeto a través de sus propiedades anidadas, manteniendo todos los tipos de valor, incluso funciones.
import { cloneDeep } from 'lodash';
const clonedObject = cloneDeep(sourceObject);
console.log('clonedObject l1_1.l2_3.l3_3', clonedObject.l1_1.l2_3.l3_3);
// function l3_3() {} --> ✔️
console.log('sourceObject l1_1.l2_3.l3_3', sourceObject.l1_1.l2_3.l3_3);
// function l3_3() {} --> ✔️
Rendimiento
Copiaremos el objeto de origen 10.000 veces usando cada método para comparar el tiempo transcurrido. Comparar el uso de memoria no tiene sentido porque Object.assign y el método del spread operator no están copiando propiedades anidadas por valor.
Los resultados en mi navegador son los siguientes:
- Tiempo transcurrido de clonación con Object.assign: 4ms
- Tiempo transcurrido de clonación con Spread operator: 22ms
- Tiempo transcurrido de clonación con JSON: 47ms
- Tiempo transcurrido de clonación con Lodash: 92ms
Como puedes ver, si solo necesitas hacer un shallow clone, Object.assign es la solución más rápida, y si solo necesitas clonar valores en propiedades anidadas (no funciones o símbolos), JSON.parse(JSON.stringify()) podría ser una solución más rápida. Pero si quieres asegurarte de que todos los valores se copien, debes usar lodash o una solución similar.
¡Obtén tus propios resultados probándolo en codesandbox!
Header picture: Karen Lau
Sergio Carracedo