Aprende leyendo en orden

Referencias y copias — trampas de arrays y objetos

Aprende por qué al asignar un array u objeto ambas variables apuntan a lo mismo y editar una cambia la otra.

Intenta respaldar un pedido antes de editarlo escribiendo const backup = order;, y arreglar order después cambia backup junto con él. Esto nunca pasa con números o cadenas.

Este artículo cubre qué se pasa realmente al asignar un array u objeto, y cómo construir una copia segura.

Qué pasa la asignación — primitivos vs referencias

Lo que hace difícil rastrear este desajuste es que lo que se pasa en la asignación difiere según el tipo de valor. Aquí repasamos qué se copia tal cual en la asignación, y qué no.

Los 5 tipos — números, cadenas, booleanos, null, y undefined (llamados primitivos — valores que no son ni objetos ni arrays) — copian el propio valor cuando los asignas.

Las 2 variables no tienen relación después de esa copia, así que cambiar una nunca cambia la otra.

Los arrays y objetos son tipos por referencia (tipos que guardan información que apunta a dónde vive un valor, en vez del valor en sí). La asignación solo pasa esa información de ubicación, así que 2 variables acaban apuntando exactamente al mismo array u objeto.

Reescríbelo a través de cualquiera de los dos nombres, y estás reescribiendo esa misma cosa única de cualquier forma.

¿La asignación pasa un valor o una ubicación?
Asigna unnúmero o cadenaSe copia elpropio valor2 valoresseparadosUn arrayu objetoSolo pasa laubicación2 variablesapuntan a 1Reescribe conun nombreApunta a lamisma cosaLa otra tambiéncambia
Un primitivo copia su valor y se vuelve independiente, pero un array u objeto tiene 2 variables apuntando a la misma cosa única.
// Un primitivo copia su valor
let price = 3980;
let salePrice = price;
salePrice = 2980;
console.log(price);          // 3980 (no afectado)
console.log(salePrice);      // 2980

// Un array apunta a la misma cosa única
const cart = ["A4 Notebook"];
const savedCart = cart;
savedCart.push("Permanent Marker");
console.log(cart.length);    // 2 (cart también creció)
console.log(cart === savedCart);  // true (apuntan a lo mismo)

Incluso con const, el contenido se puede reescribir

Lo que const fija es solo hacia dónde apunta la variable. Escribe const cart = [];, luego cart.push("Permanent Marker"), y el contenido crece; const order = {}, luego order.total = 0, también funciona.

Lo prohibido es solo la reasignación que la apunte a algo totalmente distinto, como cart = [...].

Reescribe lo que creías una copia de seguridad de un pedido, y mira qué pasa. order y backup ya están declarados, con backup asignado directamente a order.

① Saca el total en una variable separada (usa let, ya que la vas a reasignar), reasigna solo esa variable a 500, y muestra esa variable seguida del total de order.

② Reescribe el total de backup a 0, y muestra el total de backup.

③ Muestra el total del order original.

④ Muestra si order y backup apuntan a lo mismo.

(Si se ejecuta correctamente, aparecerá una explicación.)

Editor JavaScript / TypeScript

Ejecutar el código para ver el resultado

Construir una copia — qué mira realmente ===

Como acabas de ver, la asignación por sí sola no te da una copia segura y separada del original. ¿Quieres un pedido separado con solo el importe cambiado, dejando el original intacto? Construye un objeto nuevo desplegando el contenido con spread syntax, como en { ...order }. Lo que vale la pena confirmar es la relación entre lo que construyes y el original.

const copiedOrder = { ...order }; construye un objeto separado, así que order === copiedOrder es false. Para arrays y objetos, === comprueba no si el contenido coincide, sino si son exactamente la misma cosa única.

Para comparar contenido, escribe tú mismo las cosas específicas que quieres comparar — el número de claves, valores individuales, etc.

=== no mira el contenido
backup = orderApuntan a lamisma cosa=== es true{ ...order }Construye algoseparado=== es falseContenidoidéntico=== no mirael contenidoSigue false
Es true cuando apuntan a lo mismo, y false para cosas construidas por separado, incluso con contenido idéntico.
const cart = ["A4 Notebook"];
const copiedCart = [...cart];

// Cosas separadas, así que === es false
console.log(cart === copiedCart);        // false

// Incluso con contenido idéntico, sigue false
console.log(["A"] === ["A"]);            // false

// Compara el contenido en sí, y es true
console.log(cart[0] === copiedCart[0]);  // true

// Solo true cuando apuntan a lo mismo
const savedCart = cart;
console.log(cart === savedCart);         // true

Construye un pedido separado con solo el importe cambiado, dejando el original intacto. order ya está declarado.

① Construye un objeto separado copiando el contenido de order, reescribe su total a 0, y muéstralo.

② Muestra el total del order original.

③ Muestra si order y el objeto de ① son la misma cosa.

④ Muestra si los dos tienen el mismo número de claves.

Editor JavaScript / TypeScript

Ejecutar el código para ver el resultado

Copiar no llega dentro de un valor anidado — copia superficial vs profunda

El spread syntax, hasta ahora, ha hecho copias seguras para valores primitivos como un importe. Pero copia un pedido con un objeto anidado, como los datos del cliente, y mientras el objeto exterior se vuelve uno separado, el interior sigue apuntando exactamente a lo mismo que el original. Arregla una dirección en lo que creías la copia, y la dirección del pedido original también cambia — un desajuste que no esperabas.

Lo que construye spread syntax es una copia superficial (una copia que solo duplica las propiedades del primer nivel, dejando cualquier objeto anidado apuntando a lo mismo). Los valores en el primer nivel se vuelven independientes si son primitivos, pero cuando lo que hay dentro es un objeto o array, la información de ubicación — la referencia — simplemente se copia tal cual.

Cuando necesitas que el interior también se reconstruya, usa una copia profunda (una copia que reconstruye todo, hasta los valores anidados). Escribe structuredClone(order), y cada nivel de anidamiento se reconstruye como un objeto separado.

Es una función que provee el navegador, así que funciona directamente también en la consola de este sitio. Simplemente no funciona con un objeto que contenga una función.

Hasta dónde llega realmente una copia
{ ...order }Solo reconstruyeel 1.er nivelorderId se vuelveindependienteEl customeranidadoLa referenciase copia tal cualCámbialo, y eloriginal cambiastructuredCloneReconstruyetodo hasta el fondoCámbialo, y eloriginal está a salvo
Spread syntax solo reconstruye el primer nivel. Usa structuredClone cuando necesites que el interior también sea independiente.
const order = {
  orderId: "ORD-1342",
  customer: { name: "Kenji Ross", city: "Denver" },
};

// Copia superficial: el 1.er nivel es separado, el interior sigue apuntando a lo mismo
const shallow = { ...order };
shallow.orderId = "ORD-9999";
console.log(order.orderId);                      // ORD-1342 (independiente)

shallow.customer.city = "Boulder";
console.log(order.customer.city);                // Boulder (el original también cambió)
console.log(order.customer === shallow.customer);  // true

// Copia profunda: hasta el interior se vuelve separado
const deep = structuredClone(order);
deep.customer.city = "Aurora";
console.log(order.customer.city);                // Boulder (el original está a salvo)
console.log(order.customer === deep.customer);   // false

Copia un pedido que tiene datos de cliente anidados dentro, y observa hasta dónde llega una reescritura. order ya está declarado.

① Construye una copia con spread syntax, reescribe la ciudad de la copia a "Tacoma," y muestra la ciudad del order original.

② Muestra si el customer del original y el de la copia son la misma cosa.

③ Construye una copia separada que se reconstruya hasta el fondo, reescribe su ciudad a "Renton," y muestra la ciudad del order original.

④ Muestra si el customer del original y el de la copia de ③ son la misma cosa.

Editor JavaScript / TypeScript

Ejecutar el código para ver el resultado

Impedir reescrituras — Object.freeze

Como has visto, los arrays y objetos pueden acabar modificados sin querer solo por compartir una referencia. Algunos valores, como ajustes por defecto o una tasa de impuestos, necesitan una protección distinta a const — no quieres que se reescriban después. const por sí solo no impide que el contenido se reescriba, así que necesitas una forma de marcar el propio objeto como fuera de límites para cambios.

Escribe Object.freeze(settings), y ese objeto deja de aceptar cambios, adiciones o eliminaciones en sus propiedades. Intenta asignar, añadir o eliminar algo después de congelarlo, y lanza un TypeError ahí mismo, deteniendo la ejecución.

Comprueba si algo está congelado con Object.isFrozen(settings). Congelar solo llega al primer nivel, así que un objeto anidado necesita congelarse por separado.

Una reescritura tras congelar se detiene con un TypeError
Asignación antesde congelarSe aceptanormalSe vuelve elnuevo valorAsignación despuésde congelarLanza unTypeErrorLa ejecuciónse detiene ahíAñadir/eliminartras congelarLanza unTypeErrorLa ejecuciónse detiene ahí
Una vez congelado, asignar, añadir y eliminar fallan todos. Un TypeError salta ahí mismo, y nada después de esa línea se ejecuta.
const settings = { theme: "dark" };

// Se puede reescribir antes de congelar
settings.theme = "light";
console.log(settings.theme);             // light

Object.freeze(settings);
console.log(Object.isFrozen(settings));  // true

// Asignar después de congelar se detiene en esa línea
// settings.theme = "dark";
// TypeError: Cannot assign to read only property 'theme' of object '#<Object>'

// Congelar solo llega al 1.er nivel — el interior no se detiene
const nested = Object.freeze({ display: { theme: "dark" } });
nested.display.theme = "light";
console.log(nested.display.theme);       // light

// Para cambiarlo, construye un objeto nuevo en su lugar
const updated = { ...settings, theme: "dark" };
console.log(updated.theme);              // dark
console.log(settings.theme);             // light (el original está a salvo)

Para cambiarlo, construye un objeto nuevo

Asignar, añadir o eliminar en un objeto congelado lanza un TypeError, y nada después de esa línea se ejecuta. Para cambiar un objeto congelado, no lo reescribas — construye uno nuevo en su lugar, como en { ...settings, theme: "light" }.

El código de abajo intenta reescribir después unos ajustes de tasas congelados. Ejecútalo tal cual y comprueba hasta dónde llega y qué mensaje lo detiene.

Editor JavaScript / TypeScript

Ejecutar el código para ver el resultado
QUIZ

Verificación de conocimientos

Responde cada pregunta una a una.

Pregunta 1Escribe const backup = order;, luego ejecuta backup.total = 0;. ¿Qué le pasa a order.total?

Pregunta 2Con const a = ["A"]; const b = ["A"];, ¿qué devuelve a === b?

Pregunta 3Copia datos con un objeto anidado usando { ...order }. ¿Qué le pasa al objeto anidado?