Te comparto una pregunta que suele hacerse en entrevistas de JavaScript. Pone en prueba tu conocimiento sobre scope (ámbito), closures (clausuras) y la diferencia entre var
y let
.
La función printNumbers
debe imprimir los números del 0
al 9
en la consola. Sin embargo, cuando intentas ejecutar este código, solo se imprime el número 10 (10 veces). ¿Por qué?
function printNumbers() {
for (var i = 0; i < 10; i++) {
setTimeout(
function printer() {
console.log(i);
},
100 * i
);
}
}
pr****intNumbers();
¡Inténtalo tu mismo! Abre una consola de JavaScript y copia y pega el código.
Comencemos estudiando la ejecución esperada. Primero llamamos la función. La función comienza con un for-loop
que comienza en 0
y se detiene cuando i
es mayor o igual que 10
. Cada vez que se ejecuta el loop, se agenda una función que será ejecutada en 100*i
milisegundos.
Es función tiene una tarea muy fácil. Imprimir con en la consola un número. Y sin embargo, no lo hace. Peor aún, solo imprime el número 10
diez veces.
Es curioso que sea el número 10
. Este el valor que i
alcanza cuando el loop se deja de ejecutar. Pareciera entonces que la función se está recordando de i
en ese momento. Esto sucede por que el scope de i
está contenido dentro de la función printNumbers
.
Para entender mejor, escribamos una versión equivalente del mismo código.
function printNumbers() {
var i;
for (i = 0; i < 10; i++) {
setTimeout(
function printer() {
console.log(i);
},
100 * i
);
}
}
printNumbers();
Nota como la declaración de i
está fuera del for-loop
. Esto es válido por que las variables declaradas con var
obtienen “function scope”. Es decir, están disponibles dentro de todo el bloque de la función donde fueron declaradas.
Observa que todas las funciones que se ejecutan en el setTimeout
guardan una referencia a i
. Si las funciones ejecutaran inmediatamente, el valor de i
sería el esperado. Pero como la ejecución ocurrirá eventualmente, el valor de i
entonces será el último valor que obtuvo.
Para arreglar el bug, necesitamos que el valor que se vaya a imprimir viva dentro de un scope que no se comparta entre todas las funciones printer
. Esto lo podemos lograr utilizando let
en lugar de var
.
function printNumbers() {
for (let i = 0; i < 10; i++) {
// Cuando usamos let en un for-loop, es como si definieramos `i` aquí.
setTimeout(
function printer() {
console.log(i);
},
100 * i
);
}
}
printNumbers();
let
tiene un scope llamado “block scope”. Su valor es contenido dentro del bloque de código actual, o sea, el area entre {
y }
. Lo que sucede en está versión del código es que en cada loop i
es independiente, es decir, es como si estuviera definida en la primera línea del bloque del for-loop
.
Siendo así, cada i
que se utiliza en printer
es distinta. Por esta razón el valor no se comparte y obtenemos el valor que esperamos.
Es importante entender cual es el scope de las variables en nuestro código. No tener claro cómo funciona puede introducir bugs difíciles de encontrar y arreglar.
Junto al tema de scope, los closures son muy importante entenderlos. Para aprender más de ellos, profundizar tu conociemiento de scope, y de otros conceptos avanzados de JavaScript, te invito a tomar el Curso Profesional de JavaScript donde te enseño todo esto y más.
Curso Profesional de JavaScript