Sigues un error a través de tres archivos solo para descubrir que una función auxiliar inofensiva renombró a un usuario. Tú nunca tocaste el original. O eso creías. En JavaScript, asignar una variable a otra no siempre significa lo que esperas. El lenguaje divide sus datos en dos estrategias de almacenamiento, y olvidar cuál estás usando es la forma en que las mutaciones silenciosas se filtran en el código de producción.
Para prevenir esto, necesitas ver la diferencia entre los valores primitivos y los objetos exactamente como lo hace el motor.
Primitivos: Copias reales
Un primitivo es un dato único e indivisible. No puede descomponerse en valores de JavaScript más pequeños. El lenguaje define siete tipos primitivos: String, Number, Boolean, Undefined, Null, Symbol y BigInt.
Debido a que los primitivos son atómicos, generalmente residen directamente dentro de la vinculación de la variable. Cuando copias una variable primitiva a otra, el motor duplica los datos reales. Cada variable recibe su propio espacio independiente en la memoria.
let a = "Alina";
let b = a;
b = "Ali";
console.log(a); // "Alina"
console.log(b); // "Ali"
Aquí, a permanece intacta. La reasignación de b creó un valor completamente nuevo y apuntó b hacia él, mientras que a aún conserva su cadena original. Este comportamiento es copia por valor (copy by value). Funciona de la misma manera para números, booleanos, símbolos y el resto de la familia de primitivos. Puedes pasarlos a funciones, reasignarlos o devolverlos sin preocuparte por efectos secundarios en variables hermanas.
Objetos: Direcciones compartidas
Los objetos son diferentes. Un objeto es un contenedor compuesto que agrupa múltiples piezas de datos. Esta categoría incluye objetos simples, arrays, funciones, fechas y cualquier otro tipo no primitivo. Debido a que estas estructuras pueden volverse grandes y anidadas, JavaScript no almacena el objeto completo dentro de la variable. En su lugar, la variable contiene una referencia, esencialmente una dirección de memoria que apunta a los datos reales almacenados en otro lugar.
Cuando asignas un objeto a una nueva variable, el motor copia la dirección, no el objeto. Ahora, dos variables apuntan exactamente a la misma casa.
const user = { name: "Alina" };
const copy = user;
copy.name = "Ali";
console.log(user.name); // "Ali"
Cambiar copy.name también cambió user.name porque ambos nombres resuelven al mismo objeto subyacente. Esto es copia por referencia (copy by reference). La misma sorpresa ocurre con los arrays:
const scores = [82, 91, 74];
const backup = scores;
backup.push(88);
console.log(scores); // [82, 91, 74, 88]
Solo hay un array en la memoria. scores y backup son simplemente dos letreros que apuntan a él.
La palabra clave const añade una capa de confusión. Declarar un objeto con const bloquea la vinculación de la variable para que no puedas apuntarla a una nueva dirección. No bloquea el objeto en sí.
const settings = { theme: "dark" };
settings.theme = "light"; // Works perfectly.
settings = { theme: "dark" }; // TypeError
Los desarrolladores a menudo esperan que const prometa inmutabilidad. No es así. Solo evita la reasignación de la referencia. Si quieres que el contenido esté sellado, debes copiar con intención.
Dónde viven los errores reales
Los errores de referencia rara vez aparecen como reasignaciones de variables obvias. Se esconden dentro de las llamadas a funciones.
function addTimestamp(record) {
record.timestamp = Date.now();
return record;
}
const original = { id: 1 };
addTimestamp(original);
console.log(original.timestamp); // A number now exists here. Oops.
El parámetro record recibió una copia de la referencia. Cualquier mutación de propiedad dentro de la función se escribió directamente en el objeto del llamador. La función parecía una simple transformación, pero filtró el estado a través de los límites del ámbito (scope).
Este patrón es especialmente doloroso en frameworks de UI como React, donde las actualizaciones de estado dependen