Inicio / Artículos

Cierres, elevación y esto: reglas de JavaScript que aparecen en los errores

Elevación de variables, cierres y 'this' en JavaScript, explicados a través de los errores que los hacen difíciles de entender: errores TDZ, enlaces compartidos de variables en bucles y métodos desacoplados.

Editor: REACTAPP.TOP5 min de lectura1108 palabras

Fuentes: Closures, Hoisting, and `this`: The JavaScript Concepts That Finally Clicked for Me

Cierres, elevación y this: Los conceptos de JavaScript que siguen apareciendo

Muchos desarrolladores implementan aplicaciones full-stack en JavaScript durante meses sin considerar tres aspectos del lenguaje como algo importante en las entrevistas: la elevación, los cierres y this. Las definiciones memorizadas pueden servir para superar una selección inicial. Sin embargo, los errores en producción revelan esas deficiencias: tiempos de espera que registran el índice incorrecto, manejadores que pierden su instancia y variables que aparecen como undefined en lugar de generar errores. El costo suele ser tiempo: pasar horas analizando un código que parece correcto hasta que las reglas del lugar de llamada finalmente lo explican.

Las notas a continuación no son un glosario. Explican los momentos en que esas tres ideas dejan de ser abstractas y comienzan a explicar fallos reales. Cada sección parte de un pequeño programa que sorprende y luego indica la regla que hace inevitable esa sorpresa.

Elevación

La primera sorpresa es la siguiente:

console.log(greet()); // "Hello!"
function greet() {
  return "Hello!";
}

La llamada se realiza antes de la declaración, pero devuelve "Hello!". Un modelo mental de arriba hacia abajo indica que eso debería ser imposible. Las declaraciones de función son diferentes: antes de que se ejecuten las instrucciones, el motor eleva toda la declaración, incluido su cuerpo, por lo que greet ya está listo para ser llamado.

Un patrón similar se comporta de manera distinta:

console.log(name); // undefined
var name = "Prashant";

No hay ReferenceError, solo undefined. El motor sabía desde antes que name existía, pero la asignación aún no se había ejecutado. Ambos casos comparten un mismo mecanismo: las declaraciones son elevadas al principio de su ámbito. Las declaraciones de función llevan consigo su cuerpo. var solo trae el enlace; el inicializador permanece donde fue escrito, por lo que el valor inicial es vacío.

Cambiar el enlace a let modifica el modo de fallo:

console.log(name); // ReferenceError: Cannot access 'name' before initialization
let name = "Prashant";

Esto provoca un error. Un instinto común es decir que let “no eleva” los valores. Eso no es preciso. El enlace se crea temprano y luego queda bloqueado en la zona muerta temporal hasta la línea de declaración. El nombre existe; tocarlo antes de su inicialización es ilegal. Esa configuración por defecto es más segura que var, que asigna silenciosamente undefined y permite que el error continúe propagándose.

Cierres

A menudo, los cierres permiten detectar errores en bucles en lugar de solo mediante frases teóricas. Considere un registro diferido que debería imprimir 1, 2, 3:

for (var i = 1; i <= 3; i++) {
  setTimeout(() => {
    console.log(i);
  }, 1000);
}
// what I expected: 1, 2, 3
// what I got: 4, 4, 4

Se espera que los números sean secuenciales. El resultado es 4, 4, 4. var tiene ámbito de función, no de bloque, por lo que el bucle comparte un mismo i. Cuando los temporizadores se activan después de que finalice el bucle, esa variable compartida ya tiene el valor 4. Cada callback lee ese mismo valor final.

Hay una palabra clave que soluciona el problema:

for (let i = 1; i <= 3; i++) {
  setTimeout(() => {
    console.log(i);
  }, 1000);
}
// 1, 2, 3

let tiene ámbito de bloque, por lo que cada iteración posee un i distinto. Cada callback conserva su propia variable.

Esa combinación: una función junto con las variables a las que tiene acceso es la definición práctica de un cierre. El callback de setTimeout no es solo una función; también lleva consigo el entorno que necesita una vez finalizado el ámbito del bucle. Una vez que se espera esa retención de estado, el resultado 4, 4, 4 deja de parecerse a un error de la plataforma y comienza a verse como un estado mutable compartido.

Una vez que se comprenden, los cierres aparecen en todo el código habitual de React y Node: hooks que devuelven funciones, manejadores de eventos que leen el estado del componente y casi todos los actualizadores de useState. Esta concepto no se limita a las entrevistas; es así como funciona una gran parte del código de interfaz de usuario basado en callbacks.

this

this es confuso porque el mismo cuerpo de función puede mostrar valores diferentes según cómo se invoque, y no donde fue escrito.

Un contador de clase ilustra este problema:

class Counter {
  constructor() {
    this.count = 0;
  }
  increment() {
    this.count++;
    console.log(this.count);
  }
}const counter = new Counter();
const btn = document.querySelector("button");btn.addEventListener("click", counter.increment); // this is undefined

Llamar a increment a través de la instancia funciona. Pasar directamente counter.increment a addEventListener suele fallar porque this resulta ser undefined dentro del método. Al separar el método se produce una llamada a función sin contexto; en modo estricto dicha llamada no recibe el objeto original como this.

Dos soluciones hacen que esta regla sea fácil de recordar:

// bind it explicitly
btn.addEventListener("click", counter.increment.bind(counter));
// or wrap it in an arrow function
btn.addEventListener("click", () => counter.increment());

.bind(counter) fija el receptor. Un wrapper de flecha realiza la llamada a través de la instancia, manteniendo así la forma habitual de invocar el método. Las funciones de flecha no crean su propio this; heredan el enlace del ámbito circundante, y por eso la segunda solución no necesita un bind explícito.

Cuatro reglas relacionadas con el lugar de llamada cubren la mayoría de los casos cotidianos:

  1. Una llamada a función simple → en modo estricto, this es undefined; de lo contrario, es el objeto global.
  2. obj.method() → this es obj.
  3. .call(), .apply() o .bind() → this es el valor que se pasó explícitamente.
  4. Funciones de flecha → no tienen un this propio; heredan del ámbito que las contiene.

Esa última regla es la razón por la cual los manejadores de función de flecha son el valor predeterminado común dentro de clases y componentes que acceden a campos de instancia: evitan errores por desconexión accidental.

Qué realmente ayudó

Leer definiciones por sí solas rara vez sirve de algo. Las prácticas que sí funcionan son:

Escribir fragmentos pequeños y deliberadamente defectuosos: cinco o seis líneas diseñadas para causar un error específico, de modo que sea fácil observar y analizar dicho defecto.

Predecir la salida de console.log antes de ejecutar el archivo. Al forzar una predicción, una regla memorizada se convierte en una hipótesis verificable.

Explique el mecanismo en voz alta. Las lagunas aparecen de inmediato cuando se trata del hoisting o cuando las cierres deben describirse con palabras nuevas.

Conclusión

El hoisting, las cierres y this no son trivialidades. Explican los fallos en la producción: enlaces compartidos en bucles, errores TDZ y controladores que pierden su instancia. También resultan difíciles de traducir fielmente a muchos otros lenguajes, por lo que es normal sentirse confundido al principio. La creación intencionada de fallos, la predicción y la reexpresión acortan el tiempo hasta que el comportamiento parezca predecible. Mantenga un archivo temporal con fragmentos defectuosos; revisarlos después de una semana suele ser más útil que volver a leer un capítulo largo.

Lecturas relacionadas

  • Tokens de actualización y plazos límite de sesión en Keycloak explicados — Aprenda por qué Keycloak devuelve 400 al realizar actualizaciones, cómo difieren los temporizadores de sesión Idle y Max, y cómo realizar actualizaciones seguras con almacenamiento de tokens a prueba de rotación.
  • Alcance y elevación en JavaScript para ingenieros de automatización de pruebas — Parte 8 para quienes estudian pruebas: alcance global, de función y de bloque; var vs let vs const; elevación; la zona muerta temporal; y cómo estas reglas evitan fugas de datos de prueba.