const no vuelve inmutable tu objeto: lo que realmente cambia entre var, let y const

El ejemplo que más confunde a quien empieza con JavaScript suele ser corto:

const usuario = { nombre: "Ana" };
usuario.nombre = "Luis";

console.log(usuario.nombre); // Luis

Si const significa “constante”, ¿por qué cambió el nombre? La respuesta obliga a separar dos cosas que muchas explicaciones mezclan: la vinculación entre un identificador y un valor, y el contenido del objeto al que ese identificador apunta. const impide reasignar la vinculación; no congela automáticamente el objeto. Esta distinción es la clave para elegir entre var, let y const sin convertir la decisión en una regla memorizada.

La referencia de MDN sobre const lo expresa como una referencia inmutable a un valor, no como un valor necesariamente inmutable. La documentación de let y var permite completar la investigación: cada palabra crea un contrato diferente de escopo, reatribución, redeclaración y acceso antes de la declaración. Veamos qué protege realmente cada una.

Experimento 1: el mismo nombre dentro de un bloque

Empieza con un bloque if. Un bloque es el código delimitado por llaves que acompaña a una condición, un bucle o una función.

let mensaje = "fuera";

if (true) {
  let mensaje = "dentro";
  console.log(mensaje); // dentro
}

console.log(mensaje); // fuera

El let interior crea otra vinculación que vive en el bloque. Cuando termina la llave, la variable exterior sigue intacta. const sigue la misma regla de escopo:

const limite = 10;

if (true) {
  const limite = 20;
  console.log(limite); // 20
}

console.log(limite); // 10

Ahora cambia ambas declaraciones por var:

var mensaje = "fuera";

if (true) {
  var mensaje = "dentro";
  console.log(mensaje); // dentro
}

console.log(mensaje); // dentro

En este caso, el bloque no crea un nuevo escopo para var. La declaración interior se refiere a la misma variable de función o de script. Este comportamiento puede ser legítimo en código antiguo, pero sorprende cuando alguien espera que las llaves aíslen el nombre. La diferencia no es una cuestión de estilo: cambia qué código puede modificar o leer el mismo identificador.

La documentación de MDN para var explica este escopo de función y muestra por qué una declaración dentro de un bloque puede seguir afectando a la función completa. Si estás revisando código heredado, busca precisamente este patrón: una variable que parece local por su indentación, pero que en realidad se comparte más allá del bloque.

Experimento 2: reasignar no es mutar

Con un valor primitivo, la diferencia parece sencilla:

let visitas = 0;
visitas = visitas + 1;

const maximo = 10;
// maximo = 11; // TypeError

let permite que el identificador pase a referirse a otro valor. const no permite esa reasignación. Pero un objeto contiene propiedades que pueden cambiar sin que el identificador apunte a otro objeto:

const cuenta = { activa: false };
cuenta.activa = true; // permitido

// cuenta = { activa: true }; // TypeError

La primera línea de modificación cambia el contenido de la estructura existente. La segunda intenta sustituir la estructura que la variable identifica. JavaScript distingue esas operaciones. Lo mismo ocurre con arrays:

const tareas = [];
tareas.push("leer documentación"); // permitido

// tareas = ["crear proyecto"]; // TypeError

Por eso no debes prometer que const protege un objeto contra cambios. Si necesitas inmutabilidad profunda, tendrás que usar otras estrategias, como crear nuevos valores, aplicar Object.freeze() con sus límites o utilizar convenciones y herramientas del proyecto. Incluso Object.freeze() no convierte de forma recursiva todos los objetos anidados en estructuras inmutables. La decisión correcta depende del contrato de datos, no de la palabra reservada por sí sola.

Experimento 3: ¿el nombre se puede usar antes?

Prueba este código:

console.log(total);
let total = 3;

El acceso produce un ReferenceError. El periodo entre el inicio del bloque y la ejecución de la declaración se conoce como temporal dead zone o TDZ. La documentación de MDN describe let y const como declaraciones que solo pueden utilizarse después del punto de declaración; por eso se suelen llamar “no hoisted” en explicaciones introductorias, aunque la implementación del lenguaje requiere una explicación más precisa que imaginar que la línea simplemente se mueve.

Con var, el comportamiento visible es diferente:

console.log(total); // undefined
var total = 3;

La declaración existe antes de la asignación del valor, por lo que el resultado es undefined. Esto no significa que puedas usar var sin cuidado. Si el programa continúa con un valor que todavía no estaba listo, el error puede aparecer mucho después y ser más difícil de rastrear. La TDZ de let y const detiene el acceso temprano con un error explícito, lo que suele hacer visible una dependencia mal ordenada.

Una buena práctica es declarar nombres cerca del lugar donde se necesitan y evitar depender de efectos de inicialización que el lector debe reconstruir desde otra parte de la función. El escopo no es solo una restricción del motor: es información para la persona que mantiene el código.

Experimento 4: redeclarar el mismo identificador

En el mismo escopo, let y const no pueden redeclarar el mismo nombre:

let estado = "cargando";
// let estado = "listo"; // SyntaxError

var permite redeclaraciones en ciertos contextos:

var estado = "cargando";
var estado = "listo";
console.log(estado); // listo

Que el lenguaje lo permita no significa que sea una buena forma de comunicar intención. Una redeclaración puede ser un vestigio de código concatenado, un nombre repetido por accidente o una decisión consciente en una función antigua. En código nuevo, el error temprano de let y const actúa como una red que evita que dos partes del mismo escopo pretendan poseer el mismo nombre.

Si ves varias declaraciones con var, no las reemplaces de forma mecánica. Comprueba si la variable se reasigna, si se espera que atraviese bloques y si alguna función depende de su valor antes de una asignación. Modernizar el código significa preservar el comportamiento intencional y hacer explícitos los contratos, no cambiar palabras como si fueran etiquetas intercambiables.

La pregunta correcta para elegir const o let

En código moderno, una regla útil es comenzar con const y cambiar a let cuando necesites reasignar el identificador. La recomendación de usar const cuando no hay reasignación aparece también en las guías de estilo enlazadas desde MDN. Su valor no está en que vuelva seguro todo lo que contiene, sino en que comunica que ese nombre conservará su referencia durante el escopo.

PreguntaElección probableContrato que comunicas
¿El identificador debe recibir otro valor?letLa referencia cambiará en algún momento
¿El identificador no se reasigna?constLa referencia permanece durante el escopo
¿El código antiguo depende de escopo de función o redeclaración?Revisar var con cuidadoExiste un contrato legado que no conviene ocultar
¿El objeto debe ser realmente inmutable?Diseño de datos adicionalconst por sí solo no basta

Observa la precisión: no decimos “usa siempre const” ni “let es inseguro”. let es apropiado para un contador, un acumulador o un estado que debe cambiar de referencia. const es apropiado para una función, una configuración que no se reasigna o un array que vas a modificar mediante sus métodos sin sustituirlo. La palabra expresa el movimiento esperado del nombre; el tipo de dato y su diseño expresan la mutabilidad del contenido.

Una función muestra la diferencia entre contrato y accidente

function obtenerEstado() {
  let resultado = "pendiente";

  if (hayDatos()) {
    resultado = "listo";
  }

  return resultado;
}

Aquí let tiene sentido porque el identificador resultado recibe otra cadena. Si lo declararas con const, tendrías que construir el valor de otra manera:

function obtenerEstado() {
  const resultado = hayDatos() ? "listo" : "pendiente";
  return resultado;
}

Las dos versiones pueden ser correctas. La segunda hace visible que el valor se decide una vez; la primera muestra un proceso con una actualización. Ninguna merece una condena automática. Elige según el flujo que quieras que lea otra persona, no según una batalla de estilos.

Con objetos, la pregunta cambia:

function crearPerfil(nombre) {
  const perfil = { nombre, etiquetas: [] };
  perfil.etiquetas.push("nuevo");
  return perfil;
}

El perfil se crea una vez y su array interno se modifica. Eso puede ser perfectamente intencional. Si tu arquitectura exige datos inmutables, debes aplicar una estrategia coherente en todas las funciones; cambiar const por let no resuelve el problema porque ambos permiten mutar el objeto. La elección de la declaración no reemplaza el diseño del estado.

Por qué var todavía aparece en proyectos reales

var no fue eliminado del lenguaje. Aparece en código anterior a ES2015, en ejemplos antiguos y en funciones que se diseñaron alrededor de su escopo. También puede aparecer en scripts donde el acceso al escopo global tiene un comportamiento específico. El hecho de que el código moderno prefiera let y const no convierte cada uso de var en un bug.

Al revisar un proyecto, localiza tres riesgos: una declaración dentro de un bloque que se usa fuera de él, una variable leída antes de recibir su valor y una redeclaración que oculta un cambio de intención. Si ninguno aparece, la migración puede ser de bajo riesgo, pero sigue siendo necesario ejecutar pruebas. Si aparecen, primero entiende el flujo y después decide cómo hacer el contrato explícito.

Esta forma de revisar conecta con nuestros artículos sobre conceptos básicos de JavaScript y errores comunes de principiantes. Si quieres ver cómo la mutación aparece en operaciones cotidianas con colecciones, puedes continuar con map(), filter() y reduce(). El objetivo no es tener una palabra “correcta” en cada línea; es conseguir que el alcance, la reatribución y la mutación sean previsibles para quien lea el programa.

Una prueba breve antes de cambiar una declaración

Cuando dudes entre var, let y const, escribe cuatro respuestas junto al código:

  1. ¿En qué escopo debe existir este nombre?
  2. ¿Debe volver a apuntar a otro valor?
  3. ¿El valor es un primitivo, un objeto o un array?
  4. ¿Qué debe ocurrir si alguien intenta usarlo antes o redeclararlo?

Si el nombre debe vivir en un bloque y no se reasigna, const suele describirlo bien. Si cambia de referencia, let muestra ese hecho. Si aparece var, pregunta qué comportamiento antiguo se está aprovechando o tolerando. Si el contenido no puede mutar, diseña una estructura inmutable; no esperes que el nombre de la variable haga el trabajo por ti.

La palabra reservada es una señal para el lector

Aprender estas tres declaraciones no consiste en recitar cinco diferencias. Consiste en leer el programa como un mapa de identidades: dónde nace un nombre, cuánto tiempo vive, si puede apuntar a otra cosa y si lo que apunta puede cambiar por dentro. var, let y const ofrecen señales distintas para ese mapa.

La próxima vez que veas const delante de un objeto, no asumas que todo está congelado. Comprueba si cambia la referencia o una propiedad. Cuando veas let, busca el punto de reasignación. Cuando veas var, revisa el escopo real y el orden de inicialización. Esa observación convierte una regla de estilo en una herramienta de depuración.

Preguntas frecuentes sobre referencias y alcance

¿Debo cambiar todo var por let o const? No de forma automática. Revisa el escopo, las redeclaraciones, las lecturas tempranas y las pruebas del proyecto. En código nuevo suele ser más claro usar declaraciones léxicas, pero una migración segura debe conservar el comportamiento intencional.

¿Puedo modificar un objeto declarado con const? Sí. Puedes cambiar sus propiedades o modificar un array. Lo que no puedes hacer es reasignar el identificador para que apunte a otro objeto o array.

¿Cuál es la diferencia entre reasignación y mutación? Reasignar cambia el valor al que apunta el identificador; mutar cambia el contenido del objeto o array existente. const impide la primera operación, pero no la segunda por sí sola.

¿Qué es la temporal dead zone? Es el periodo desde el inicio del escopo hasta la declaración de una variable let o const. Acceder al nombre durante ese periodo produce un error, aunque la declaración exista en el código.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio