Una de las diferencias fundamentales entre los objetos y los primitivos es que los objetos se almacenan y copian "por referencia", mientras que los valores primitivos (cadenas, números, booleanos, etc.) siempre se copian "como un valor completo".
Esto es fácil de entender si miramos un poco más allá de lo que sucede cuando copiamos un valor.
Comencemos con una primitiva, como una cadena.
Aquí ponemos una copia del message
en phrase
:
dejar mensaje = "¡Hola!"; let frase = mensaje;
Como resultado tenemos dos variables independientes, cada una de las cuales almacena la cadena "Hello!"
.
Un resultado bastante obvio, ¿verdad?
Los objetos no son así.
Una variable asignada a un objeto no almacena el objeto en sí, sino su “dirección en la memoria”; en otras palabras, “una referencia” al mismo.
Veamos un ejemplo de dicha variable:
dejar usuario = { nombre: "Juan" };
Y así es como se almacena realmente en la memoria:
El objeto se almacena en algún lugar de la memoria (a la derecha de la imagen), mientras que la variable user
(a la izquierda) tiene una "referencia" a él.
Podemos pensar en una variable de objeto, como user
, como una hoja de papel con la dirección del objeto.
Cuando realizamos acciones con el objeto, por ejemplo, tomamos una propiedad user.name
, el motor JavaScript mira lo que hay en esa dirección y realiza la operación en el objeto real.
He aquí por qué es importante.
Cuando se copia una variable de objeto, se copia la referencia, pero el objeto en sí no se duplica.
Por ejemplo:
dejar usuario = { nombre: "Juan" }; dejar administrador = usuario; //copiar la referencia
Ahora tenemos dos variables, cada una almacena una referencia al mismo objeto:
Como puedes ver, todavía hay un objeto, pero ahora con dos variables que hacen referencia a él.
Podemos usar cualquiera de las variables para acceder al objeto y modificar su contenido:
dejar usuario = { nombre: 'Juan' }; dejar administrador = usuario; admin.nombre = 'Pete'; // cambiado por la referencia "admin" alerta(nombre.usuario); // 'Pete', los cambios se ven desde la referencia "usuario"
Es como si tuviéramos un armario con dos llaves y usáramos una de ellas ( admin
) para entrar en él y hacer cambios. Luego, si luego usamos otra clave ( user
), seguiremos abriendo el mismo gabinete y podremos acceder al contenido modificado.
Dos objetos son iguales sólo si son el mismo objeto.
Por ejemplo, aquí a
y b
hacen referencia al mismo objeto, por lo que son iguales:
sea a = {}; sea b = a; //copiar la referencia alerta( a == b ); // verdadero, ambas variables hacen referencia al mismo objeto alerta( a === b ); // verdadero
Y aquí dos objetos independientes no son iguales, aunque parezcan iguales (ambos están vacíos):
sea a = {}; sea b = {}; // dos objetos independientes alerta( a == b ); // FALSO
Para comparaciones como obj1 > obj2
o para una comparación con un obj == 5
primitivo, los objetos se convierten en primitivos. Estudiaremos cómo funcionan las conversiones de objetos muy pronto, pero a decir verdad, este tipo de comparaciones rara vez son necesarias; normalmente aparecen como resultado de un error de programación.
Los objetos constantes se pueden modificar.
Un efecto secundario importante de almacenar objetos como referencias es que un objeto declarado como const
se puede modificar.
Por ejemplo:
usuario constante = { nombre: "Juan" }; nombre.usuario = "Pete"; // (*) alerta(nombre.usuario); // Pete
Podría parecer que la línea (*)
provocaría un error, pero no es así. El valor de user
es constante, siempre debe hacer referencia al mismo objeto, pero las propiedades de ese objeto pueden cambiar libremente.
En otras palabras, el const user
da un error sólo si intentamos configurar user=...
como un todo.
Dicho esto, si realmente necesitamos hacer que las propiedades de los objetos sean constantes, también es posible, pero usando métodos totalmente diferentes. Lo mencionaremos en el capítulo Indicadores y descriptores de propiedades.
Entonces, copiar una variable de objeto crea una referencia más al mismo objeto.
¿Pero qué pasa si necesitamos duplicar un objeto?
Podemos crear un nuevo objeto y replicar la estructura del existente, iterando sobre sus propiedades y copiándolas en el nivel primitivo.
Como esto:
dejar usuario = { nombre: "Juan", edad: 30 }; dejar clonar = {}; // el nuevo objeto vacío // copiemos todas las propiedades del usuario en él para (dejar ingresar usuario) { clonar[clave] = usuario[clave]; } // ahora clon es un objeto totalmente independiente con el mismo contenido clon.nombre = "Pete"; //cambió los datos en él alerta (nombre.usuario); // todavía John en el objeto original
También podemos usar el método Object.assign.
La sintaxis es:
Object.assign(destino, ...fuentes)
El primer argumento dest
es un objeto de destino.
Otros argumentos son una lista de objetos fuente.
Copia las propiedades de todos los objetos de origen en el dest
de destino y luego lo devuelve como resultado.
Por ejemplo, tenemos un objeto user
, agreguemos un par de permisos:
dejar usuario = { nombre: "Juan" }; let permisos1 = {canView: verdadero}; let permisos2 = {canEdit: verdadero}; // copia todas las propiedades de permisos1 y permisos2 en el usuario Object.assign(usuario, permisos1, permisos2); // ahora usuario = { nombre: "John", canView: verdadero, canEdit: verdadero } alerta(nombre.usuario); // John alerta(usuario.canView); // verdadero alerta(usuario.canEdit); // verdadero
Si el nombre de la propiedad copiada ya existe, se sobrescribe:
dejar usuario = { nombre: "Juan" }; Object.assign(usuario, { nombre: "Pete" }); alerta(nombre.usuario); // ahora usuario = { nombre: "Pete" }
También podemos usar Object.assign
para realizar una clonación de objetos simple:
dejar usuario = { nombre: "Juan", edad: 30 }; let clone = Object.assign({}, usuario); alerta(clon.nombre); // John alerta(clon.edad); // 30
Aquí copia todas las propiedades del user
en el objeto vacío y lo devuelve.
También existen otros métodos para clonar un objeto, por ejemplo, usando la sintaxis extendida clone = {...user}
, que se explica más adelante en el tutorial.
Hasta ahora asumimos que todas las propiedades del user
son primitivas. Pero las propiedades pueden ser referencias a otros objetos.
Como esto:
dejar usuario = { nombre: "Juan", tamaños: { altura: 182, ancho: 50 } }; alerta (usuario.tamaños.altura); // 182
Ahora no es suficiente copiar clone.sizes = user.sizes
, porque user.sizes
es un objeto y se copiará por referencia, por lo que clone
y user
compartirán los mismos tamaños:
dejar usuario = { nombre: "Juan", tamaños: { altura: 182, ancho: 50 } }; let clone = Object.assign({}, usuario); alerta (usuario.tamaños === clon.tamaños); // verdadero, mismo objeto // tamaños de recursos compartidos de usuarios y clones usuario.tamaños.ancho = 60; // cambiar una propiedad desde un lugar alerta(clon.tamaños.ancho); // 60, obtienes el resultado del otro
Para solucionar eso y hacer que user
y clone
sean objetos verdaderamente separados, debemos usar un bucle de clonación que examine cada valor de user[key]
y, si es un objeto, luego replicar también su estructura. A eso se le llama “clonación profunda” o “clonación estructurada”. Existe un método estructuradoClone que implementa la clonación profunda.
La llamada structuredClone(object)
clona el object
con todas las propiedades anidadas.
Así es como podemos usarlo en nuestro ejemplo:
dejar usuario = { nombre: "Juan", tamaños: { altura: 182, ancho: 50 } }; let clone = estructuradoClone(usuario); alerta (usuario.tamaños === clon.tamaños); // falso, objetos diferentes // el usuario y el clon ahora no tienen ninguna relación usuario.tamaños.ancho = 60; // cambiar una propiedad desde un lugar alerta(clon.tamaños.ancho); // 50, no relacionado
El método structuredClone
puede clonar la mayoría de los tipos de datos, como objetos, matrices y valores primitivos.
También admite referencias circulares, cuando una propiedad de objeto hace referencia al objeto mismo (directamente o mediante una cadena de referencias).
Por ejemplo:
dejar usuario = {}; // creemos una referencia circular: // user.me hace referencia al propio usuario usuario.yo = usuario; let clone = estructuradoClone(usuario); alerta(clon.me === clonar); // verdadero
Como puede ver, clone.me
hace referencia al clone
, no al user
. Entonces la referencia circular también se clonó correctamente.
Sin embargo, hay casos en los que structuredClone
falla.
Por ejemplo, cuando un objeto tiene una propiedad de función:
// error clon estructurado({ f: función() {} });
Las propiedades de función no son compatibles.
Para manejar casos tan complejos, es posible que necesitemos usar una combinación de métodos de clonación, escribir código personalizado o, para no reinventar la rueda, tomar una implementación existente, por ejemplo _.cloneDeep(obj) de la biblioteca JavaScript lodash.
Los objetos se asignan y copian por referencia. En otras palabras, una variable no almacena el "valor del objeto", sino una "referencia" (dirección en la memoria) del valor. Entonces, copiar dicha variable o pasarla como argumento de función copia esa referencia, no el objeto en sí.
Todas las operaciones a través de referencias copiadas (como agregar/eliminar propiedades) se realizan en el mismo objeto.
Para hacer una “copia real” (un clon) podemos usar Object.assign
para la llamada “copia superficial” (los objetos anidados se copian por referencia) o una función de “clonación profunda” structuredClone
o usar una implementación de clonación personalizada, como como _.cloneDeep(obj).