Clausuras en bucles: cómo no volverse loco al capturar variables

Entrenador de Python en Línea para Principiantes

Aprende Python fácilmente sin sobrecargarte de teoría. Resuelve tareas prácticas con verificación automática, recibe pistas y escribe código directamente en el navegador, sin instalar nada.

Start Course

¿Por qué tu código JavaScript se comporta como un teléfono descompuesto?

Imagina: escribes un bucle simple, agregas manejadores de eventos a los botones, y de repente —¡bam!— todos los botones muestran el mismo número. ¿Te suena? Si alguna vez te has enfrentado a clausuras en bucles JavaScript, conoces ese momento de disonancia cognitiva. Estás seguro de que el código es correcto, pero el navegador insiste obstinadamente: "No, amigo, te has equivocado en algo".

En Stack Overflow esta pregunta ha acumulado cientos de miles de visitas. Y no es de extrañar: capturar variables de bucle en una clausura es una de las trampas más comunes para principiantes e incluso desarrolladores experimentados. Analicemos por qué sucede esto y, lo más importante, cómo solucionarlo teniendo en cuenta el rendimiento.



El problema clásico: var y asincronía

Observa este código inofensivo:

for (var i = 0; i < 5; i++) {    setTimeout(function() {        console.log(i); // ¿Qué mostrará?    }, 100);}

Si respondiste "0, 1, 2, 3, 4" —felicidades, eres un optimista. La realidad es cruel: la consola mostrará 5, 5, 5, 5, 5. ¿Por qué? Porque var tiene ámbito de función, no de bloque. Cuando setTimeout se ejecuta, el bucle ya ha terminado y i es igual a 5. Las cinco clausuras hacen referencia a la misma variable.

Este es un ejemplo clásico del problema de clausuras en bucles JavaScript. Y la solución está en la superficie: necesitas crear un nuevo ámbito para cada iteración.



Solución n.º 1: IIFE — el viejo y confiable parche

Antes de la llegada de ES6, los desarrolladores usaban funciones inmediatamente invocadas (IIFE):

for (var i = 0; i < 5; i++) {    (function(j) {        setTimeout(function() {            console.log(j);        }, 100);    })(i);}

Aquí, j es una copia de i en el momento de la invocación de la IIFE. Cada clausura captura su propio valor. ¿Funciona? Sí. ¿Es bonito? No mucho. Desde el punto de vista de la optimización de clausuras, esto crea una función adicional en cada iteración, lo que puede afectar la memoria si el bucle es enorme.



Solución n.º 2: let — el salvador de ES6

Con la llegada de let, el problema se resolvió de manera elegante:

for (let i = 0; i < 5; i++) {    setTimeout(function() {        console.log(i); // 0, 1, 2, 3, 4    }, 100);}

let crea un nuevo enlace para cada iteración del bucle. Este es un comportamiento integrado, establecido en la especificación de ECMAScript. Cada clausura obtiene su propia copia de i. ¿Rendimiento? Excelente: el motor lo optimiza a nivel de compilación.

Sin embargo, hay un matiz: si usas let dentro del bucle pero fuera del bloque for, el comportamiento puede diferir. Mantén siempre let dentro del encabezado del bucle.



Solución n.º 3: bind — enfoque funcional

El método bind permite fijar argumentos:

for (var i = 0; i < 5; i++) {    setTimeout(function(j) {        console.log(j);    }.bind(null, i), 100);}

Aquí, bind crea una nueva función con un argumento predefinido. Esto funciona, pero crea una función adicional en cada iteración. En cuanto a rendimiento, bind es ligeramente más lento que IIFE debido a las comprobaciones internas.



Solución n.º 4: forEach y otros métodos de arrays

A veces, la mejor manera de evitar el problema es no usar for:

[0, 1, 2, 3, 4].forEach(function(i) {    setTimeout(function() {        console.log(i);    }, 100);});

forEach crea un nuevo ámbito para cada iteración a través del callback. Esto no solo resuelve

Blogs

Book Recommendations