Valor y referencia en JavaScript

En esta página
Enterate del próximo post
Un mail cuando publico algo nuevo. Sin spam y te podés dar de baja cuando quieras.

Un mail cuando publico algo nuevo. Sin spam y te podés dar de baja cuando quieras.
Un compañero de curso me mandó un bug la semana pasada: modificó una copia de un objeto de configuración, y el objeto original cambió con ella, sin que nadie lo tocara a propósito. No había ningún error de sintaxis. Era el modelo de comparación de JavaScript funcionando exactamente como está diseñado.
let numero = 5;
let copiaNumero = numero;
copiaNumero = 10;
numero; // 5 ← no cambia
copiaNumero; // 10Con un primitivo, copiar la variable copia el valor entero. Las dos variables quedan totalmente separadas después de la asignación.
let config = { tema: "oscuro" };
let copiaConfig = config;
copiaConfig.tema = "claro";
config.tema; // "claro" ← el original también cambió
copiaConfig.tema; // "claro"Con un objeto, lo que guarda la variable no es el objeto: es una referencia a él, algo parecido a una dirección. Copiar la variable copia esa dirección, no lo que hay ahí. config y copiaConfig terminan apuntando al mismo objeto, así que modificar uno modifica el otro. Esto vale igual para objetos, arrays y funciones: los tres son, por debajo, el mismo tipo de valor.
=== compara la referencia, no el contenidoconst a = { valor: 5 };
const b = { valor: 5 };
const c = a;
a === b; // false ← mismo contenido, objetos distintos
a === c; // true ← misma referenciaa y b tienen el mismo contenido pero son dos objetos creados por separado, así que === da false. a y c son la misma referencia, así que da true: c no es una copia de a, es otro nombre para el mismo objeto.
JSON.stringify(a) === JSON.stringify(b); // true ← compara el texto, no la referencia
const conFuncion = { f: () => {} };
JSON.stringify(conFuncion); // "{}" ← la función desaparecióConvertir a texto es una forma rápida de comparar contenido, pero tiene costo: JSON.stringify ignora las funciones, los symbol y las propiedades con valor undefined, y el orden de las claves importa aunque el contenido sea el mismo. En JavaScript no existe una función nativa que compare dos objetos por contenido de forma completa y correcta. Por eso existen librerías dedicadas solo a esto.
const protege la referencia, no el contenidoconst persona = { nombre: "Fernando" };
persona.nombre = "Fer"; // funciona: no reasigna la variable
persona.nombre; // "Fer"En el post de objetos literales usamos este mismo patrón, persona.edad = 24 con persona declarado como const, y funcionó sin ningún error. Ahora ya sabemos por qué: la variable siguió apuntando exactamente al mismo objeto. Lo único que cambió fue el contenido de ese objeto, no la referencia.
const persona2 = { nombre: "Fernando" };
persona2 = { nombre: "Otro" };
// TypeError: Assignment to constant variable.Esto sí falla, porque acá se intenta cambiar a qué objeto apunta persona2, y eso es exactamente lo que const prohíbe. La regla completa: const fija la referencia, no lo que hay del otro lado.
const original = { nombre: "Fernando", direccion: { ciudad: "La Plata" } };
const copia = { ...original };
copia.nombre = "Fer";
copia.direccion.ciudad = "CABA";
original.nombre; // "Fernando" ← no cambió
original.direccion.ciudad; // "CABA" ← sí cambióEn el post de métodos de objetos ya quedó anotado que el spread y Object.assign hacen copia superficial. Acá está el porqué y hasta dónde llega. El spread sí crea un objeto nuevo en el primer nivel: reasignar copia.nombre no toca original. Pero direccion es en sí mismo un objeto, y el spread no lo copió: copió la referencia a él. original.direccion y copia.direccion son el mismo objeto, así que modificarlo desde cualquiera de los dos lados se nota en ambos.
const copiaProfunda = structuredClone(original);
copiaProfunda.direccion.ciudad = "Rosario";
original.direccion.ciudad; // "CABA" ← esta vez no cambió
copiaProfunda.direccion.ciudad; // "Rosario"structuredClone sí copia en profundidad: recorre el objeto entero y crea copias nuevas en cada nivel, no solo en el primero. El costo es que no puede clonar cualquier cosa.
structuredClone({ f: () => {} });
// DOMException [DataCloneError]: could not be cloned
class Persona {
constructor(nombre) { this.nombre = nombre; }
}
const clon = structuredClone(new Persona("Fernando"));
clon instanceof Persona; // false ← volvió como objeto planoUna función no se puede clonar y directamente tira un error. Y si el objeto es una instancia de una clase propia, la copia pierde la clase: vuelve como un objeto plano, sin conexión con el prototipo original.
const original = [1, 2, 3];
const alias = original;
alias.push(4);
original; // [1, 2, 3, 4] ← el push afectó al originalUn array es un objeto con índices numéricos como claves, así que todo lo anterior aplica igual: asignar copia la referencia, no la lista.
const copiaReal = [...original];
copiaReal.push(5);
original; // [1, 2, 3, 4] ← esta vez no cambió
copiaReal; // [1, 2, 3, 4, 5]El spread en un array resuelve el mismo problema que en un objeto: crea uno nuevo. Y tiene la misma limitación: si los elementos del array son objetos, esos objetos se comparten entre el original y la copia.
function incrementar(numero) {
numero = numero + 1;
}
let edad = 24;
incrementar(edad);
edad; // 24 ← no cambióLa función recibe una copia del valor. Cambiar esa copia adentro de la función no afecta a la variable de afuera. Hasta acá, nada distinto de lo ya visto.
function reasignar(persona) {
persona = { nombre: "Otro" };
}
function renombrar(persona) {
persona.nombre = "Otro";
}
const persona = { nombre: "Fernando" };
reasignar(persona);
persona.nombre; // "Fernando" ← no cambió
renombrar(persona);
persona.nombre; // "Otro" ← sí cambióJavaScript siempre pasa argumentos por valor. La diferencia está en qué valor se copia. Con un primitivo se copia el dato entero. Con un objeto se copia la referencia, no el objeto. Por eso reasignar no tiene ningún efecto afuera de la función: solo cambia a qué apunta la copia local de la referencia, y esa copia se descarta cuando la función termina. renombrar, en cambio, usa esa misma copia de la referencia para llegar hasta el único objeto que existe y lo modifica ahí. Es habitual llamar a esto "paso por referencia", pero no es preciso: JavaScript pasa siempre por valor, y ese valor puede ser una referencia.
Los objetos, los arrays y las funciones se comparan y se copian por referencia, no por contenido. Asignar, pasar como argumento o copiar con spread mueve la referencia, nunca el valor completo, y eso explica por qué una copia superficial sigue compartiendo lo que tiene adentro.
Si estás empezando, quedate con tres cosas: === en un objeto compara identidad, no contenido; una copia con spread es superficial, así que si necesitás una copia real hace falta structuredClone u otra herramienta pensada para copiar en profundidad; y const fija la variable, no lo que hay del otro lado.
En el próximo post de la serie voy a los casos donde la comparación deja de ser tan prolija: la coerción de tipos y el algoritmo real detrás de ==.