Los objetos son los bloques fundamentales de JavaScript. Un objeto es una colección de propiedades, y una propiedad es una asociación entre una clave (o nombre) y un valor. Casi todos los objetos en JavaScript son instancias de Object
que se encuentra en la parte superior de la cadena de prototipos.
Introducción
Como sabes, el operador de asignación no crea una copia de un objeto, sólo le asigna una referencia, veamos el siguiente código:
let obj = { a: 1, b: 2,};let copy = obj;obj.a = 5;console.log(copy.a);// Result // a = 5;
La variable obj
es un contenedor para el nuevo objeto inicializado. La variable copy
apunta al mismo objeto y es una referencia a ese objeto. Así que básicamente este objeto { a: 1, b: 2, }
está diciendo: Ahora hay dos formas de acceder a mí. Tienes que pasar a través de la variable obj
o de la variable copy
de cualquier manera sigues llegando a mí y cualquier cosa que me hagas a través de estas formas (puertas de enlace) me afectará.
¡Se habla mucho de la inmutabilidad estos días y tienes que escuchar esta llamada! Este método elimina cualquier forma de inmutabilidad y podría dar lugar a errores en caso de que el objeto original sea utilizado por otra parte de tu código.
La forma ingenua de copiar objetos
La forma ingenua de copiar objetos es hacer un bucle a través del objeto original y copiar cada propiedad una tras otra. Echemos un vistazo a este código:
function copy(mainObj) { let objCopy = {}; // objCopy will store a copy of the mainObj let key; for (key in mainObj) { objCopy = mainObj; // copies each property to the objCopy object } return objCopy;}const mainObj = { a: 2, b: 5, c: { x: 7, y: 4, },}console.log(copy(mainObj));
Asuntos inherentes
-
objCopy
el objeto tiene un nuevo métodoObject.prototype
diferente al método del prototipo del objetomainObj
, que no es lo que queremos. Queremos una copia exacta del objeto original. - Los descriptores de propiedades no se copian. Un descriptor «escribible» con valor establecido como falso será verdadero en el objeto
objCopy
. - El código anterior sólo copia las propiedades enumerables de
mainObj
. - Si una de las propiedades del objeto original es un objeto en sí, entonces se compartirá entre la copia y el original haciendo que sus respectivas propiedades apunten al mismo objeto.
Copia superficial de objetos
Se dice que un objeto está copiado superficialmente cuando las propiedades de nivel superior del origen se copian sin ninguna referencia y existe una propiedad de origen cuyo valor es un objeto y se copia como referencia. Si el valor de origen es una referencia a un objeto, sólo se copia ese valor de referencia al objeto de destino.
Una copia superficial duplicará las propiedades de nivel superior, pero el objeto anidado se comparte entre el original(fuente) y la copia(destino).
Usando el método Object.assign()
El método Object.assign() se utiliza para copiar los valores de todas las propiedades propias enumerables de uno o varios objetos origen a un objeto destino.
let obj = { a: 1, b: 2,};let objCopy = Object.assign({}, obj);console.log(objCopy);// Result - { a: 1, b: 2 }
Bien, esto hace el trabajo hasta ahora. Hemos hecho una copia de obj
. Veamos si existe la inmutabilidad:
let obj = { a: 1, b: 2,};let objCopy = Object.assign({}, obj);console.log(objCopy); // result - { a: 1, b: 2 }objCopy.b = 89;console.log(objCopy); // result - { a: 1, b: 89 }console.log(obj); // result - { a: 1, b: 2 }
En el código anterior, cambiamos el valor de la propiedad 'b'
en el objeto objCopy
a 89
y cuando registramos el objeto objCopy
modificado en la consola, los cambios sólo se aplican a objCopy
. La última línea de código comprueba que el objeto obj
sigue intacto y no ha cambiado. Esto implica que hemos creado con éxito una copia del objeto fuente sin ninguna referencia a él.
Caída de Object.assign()
¡No tan rápido! Aunque hemos creado con éxito una copia y todo parece funcionar bien, ¿recuerdas que hablamos de la copia superficial? Veamos este ejemplo:
let obj = { a: 1, b: { c: 2, },}let newObj = Object.assign({}, obj);console.log(newObj); // { a: 1, b: { c: 2} }obj.a = 10;console.log(obj); // { a: 10, b: { c: 2} }console.log(newObj); // { a: 1, b: { c: 2} }newObj.a = 20;console.log(obj); // { a: 10, b: { c: 2} }console.log(newObj); // { a: 20, b: { c: 2} }newObj.b.c = 30;console.log(obj); // { a: 10, b: { c: 30} }console.log(newObj); // { a: 20, b: { c: 30} }// Note: newObj.b.c = 30; Read why..
¿Por qué obj.b.c = 30?
Bueno, eso es una trampa de Object.assign()
Object.assign
sólo hace copias superficiales. Tanto newObj.b
como obj.b
comparten la misma referencia al objeto porque no se hicieron copias individuales, sino que se copió una referencia al objeto. Cualquier cambio realizado en cualquiera de las propiedades del objeto se aplica a todas las referencias que utilizan el objeto. ¿Cómo podemos solucionar esto? Sigue leyendo… tenemos una solución en la siguiente sección.
Nota: Las propiedades en la cadena de prototipos y las propiedades no enumerables no pueden ser copiadas. Ver aquí:
let someObj = { a: 2,}let obj = Object.create(someObj, { b: { value: 2, }, c: { value: 3, enumerable: true, },});let objCopy = Object.assign({}, obj);console.log(objCopy); // { c: 3 }
-
someObj
está en la cadena de prototipos de obj por lo que no se copiaría. -
property b
es una propiedad no enumerable. -
property c
tiene un descriptor de propiedad enumerable que le permite ser enumerable. Por eso se ha copiado.
Copia profunda de objetos
Una copia profunda duplicará todos los objetos que encuentre. La copia y el objeto original no compartirán nada, por lo que será una copia del original. Aquí está la solución al problema que encontramos usando Object.assign()
. Vamos a explorar.
Usando JSON.parse(JSON.stringify(object));
Esto soluciona el problema que teníamos antes. ¡Ahora newObj.b
tiene una copia y no una referencia! Esta es una forma de copiar en profundidad los objetos. Aquí tienes un ejemplo:
let obj = { a: 1, b: { c: 2, },}let newObj = JSON.parse(JSON.stringify(obj));obj.b.c = 20;console.log(obj); // { a: 1, b: { c: 20 } }console.log(newObj); // { a: 1, b: { c: 2 } } (New Object Intact!)
Inmutable: ✓
Pitfall
Desgraciadamente, este método no se puede utilizar para copiar métodos de objetos definidos por el usuario. Ver más abajo.
Copiar métodos de objetos
Un método es una propiedad de un objeto que es una función. En los ejemplos hasta ahora, no hemos copiado un objeto con un método. Vamos a intentarlo ahora y a utilizar los métodos que hemos aprendido para hacer copias.
let obj = { name: 'scotch.io', exec: function exec() { return true; },}let method1 = Object.assign({}, obj);let method2 = JSON.parse(JSON.stringify(obj));console.log(method1); //Object.assign({}, obj)/* result{ exec: function exec() { return true; }, name: "scotch.io"}*/console.log(method2); // JSON.parse(JSON.stringify(obj))/* result{ name: "scotch.io"}*/
El resultado muestra que Object.assign()
se puede utilizar para copiar métodos mientras que JSON.parse(JSON.stringify(obj))
no se puede utilizar.
Copiar objetos circulares
Los objetos circulares son objetos que tienen propiedades que se referencian a sí mismos. Vamos a utilizar los métodos de copia de objetos que hemos aprendido hasta ahora para hacer copias de un objeto circular y ver si funciona.
Usando JSON.parse(JSON.stringify(object))
Probemos JSON.parse(JSON.stringify(object))
:
// circular objectlet obj = { a: 'a', b: { c: 'c', d: 'd', },}obj.c = obj.b;obj.e = obj.a;obj.b.c = obj.c;obj.b.d = obj.b;obj.b.e = obj.b.c;let newObj = JSON.parse(JSON.stringify(obj));console.log(newObj);
Aquí está el resultado:
JSON.parse(JSON.stringify(obj))
claramente no funciona para objetos circulares.
Usando Object.assign()
Probemos con Object.assign()
:
// circular objectlet obj = { a: 'a', b: { c: 'c', d: 'd', },}obj.c = obj.b;obj.e = obj.a;obj.b.c = obj.c;obj.b.d = obj.b;obj.b.e = obj.b.c;let newObj2 = Object.assign({}, obj);console.log(newObj2);
Aquí está el resultado:
Object.assign()
funciona bien para la copia superficial de objetos circulares pero no funcionaría para la copia profunda. Siéntete libre de explorar el circular object tree
en la consola de tu navegador. Estoy seguro de que encontrarás un montón de trabajo interesante allí.
Usando elementos de propagación ( … )
ES6 ya tiene implementados elementos de reposo para la asignación de desestructuración de arrays y elementos de propagación para literales de arrays. Echa un vistazo a la implementación de elementos spread en un array aquí:
const array = ;const newArray = ;console.log(newArray);// Result //
La propiedad spread para literales de objetos es actualmente una propuesta de la fase 3 de ECMAScript. Las propiedades de propagación en los inicializadores de objetos copian las propiedades enumerables propias de un objeto de origen en el objeto de destino. El siguiente ejemplo muestra lo fácil que sería copiar un objeto una vez que la propuesta haya sido aceptada.
let obj = { one: 1, two: 2,}let newObj = { ...z };// { one: 1, two: 2 }
Nota: Esto sólo será efectivo para la copia superficial
Conclusión
Copiar objetos en JavaScript puede ser bastante desalentador especialmente si eres nuevo en JavaScript y no conoces el lenguaje. Esperemos que este artículo te haya ayudado a entender y a evitar futuros escollos que puedas encontrar copiando objetos. Si tienes alguna librería o pieza de código que logre un mejor resultado, siéntete bienvenido a compartirlo con la comunidad. Feliz codificación!