Note: Before you read this article, i suggest you to read: How Closures Work in Javascript. Reading that article will give you the background knowledge you need to understand this article.
"Closures in Loop" problem is a familiar problem among javascript developers. If you aren't familiar with this problem, look at the following code example:
function foo() {
for (var i = 1; i <= 3; i++) {
setTimeout(function () {
console.log(i);
}, 1000);
}
}
foo();
You might expect above code to output:
1
2
3
but the actual output is:
4
4
4
The reason for such an output is that by the time callback function of each setTimeout
call is executed, loop has already finished and the value of the variable i
has been incremented to 4.
As i
is declared with var
, variable i
is available throughout the function foo
, so all three callback functions close over the same variable i
and they all see the same value of i
which by the time callback functions execute is 4.
To fix the problem, we need a way to make each callback function close over a different copy of the variable i
.
Pre-ES6 Solution
In ES5 and earlier, usual way to get around this problem was to use an IIFE (immediately invoked function expressions). Following code example shows how using an IIFE solved the problem:
function foo() {
for (var i = 1; i <= 3; i++) {
(function (counter) {
setTimeout(function () {
console.log(counter);
}, 1000);
})(i);
}
}
foo();
Above code will produce the expected output:
1
2
3
This is because, unlike the previous code example which didn't use an IIFE, callback function of the setTimeout
used the variable counter
which is the parameter of the wrapper anonymous function. In each iteration of the loop, new wrapper function is created, it is passed a new value of i
as an argument and as the value of counter
is not changed, each callback function logs the value of the counter
that was passed to its wrapper anonymous function.
ES6 Solution
Thanks to ES6, we now have a simple fix for the "Closures in Loops" problem. All we need to do is replace var
with let
.
function foo() {
for (let i = 1; i <= 3; i++) {
setTimeout(function () {
console.log(i);
}, 1000);
}
}
foo();
This change will produce the expected output:
1
2
3
How does this small change fix the problem? Callback function of setTimeout
still closes over the variable i
, then how does the callback function of each setTimeout
call sees a different value of i
?
Note: Explanation ahead assumes that you have read the following article: How Closures Work in Javascript
How let
solves this problem?
TL;DR; Replacing var
with let
solves the problem because each iteration of the loop has its own copy of the variable i
that the callback function of setTimeout
closes over. Instead of closing over the same variable i
, with let
, all three callback functions close over the different copy of the variable i
.
To understand how each iteration of the loop gets its own copy of the variable i
, let's once again look at the code that used let
to solve the "Closures in Loops" problem:
function foo() {
for (let i = 1; i <= 3; i++) {
setTimeout(function () {
console.log(i);
}, 1000);
}
}
foo();
Following steps explain how javascript engine executes the above code:
When
foo
function is called, new environment object (let's call itEnvObj1
) gets created.EnvObj1
environment object is linked to the environment that is saved in the internal [[Environment]] slot of thefoo
function which, in this case, is the global environment.Execution of the
for
loop is started by remembering the list of variables declared withlet
in the initialization part of thefor
loop. In our code example, we have just one variable, i.e.i
but more variables can be declared in the initialization part, if needed.A new environment object (let's call it
initEnvObj
) is created for the initialization part of the loop and a binding for the variablei
is created in this environment object. This environment gets environment objectEnvObj1
(created in step 1) as its outer environment.A new environment (
iter1EnvObj
) is created for the first iteration of the loop andEnvObj1
(created in step 1) is set as the outer environment of this newly created environment:iterEnvObj
.Referring to the environment created in step 3, a new binding for the variable
i
is created in theiter1EnvObj
, setting its initial value equal to the value of the variablei
in the environment object (initEnvObj
) that was created in step 4.Because the loop condition
i <= 3
is true, body of the loop is executed, creating a callback function for thesetTimeout
, saving a reference to the environment object of first loop iteration (iter1EnvObj
), in the internal [[Environment]] slot of the callback function. After that, callback function is passed to thesetTimeout
function as an argument.Following is the simplified diagram of how each environment is linked to its outer environment at this point:
At this point, first iteration of the loop is done. Now, we are ready for the second iteration of the loop.
Second iteration starts with the creation of a new environment object (let's call it
iter2EnvObj
) andenvObj1
(created in step 1) is set as its outer environment.Using the environment object
initEnvObj
(created in step 3), a new binding for variablei
is created in the environment object created for the second iteration of the loop (iter2EnvObj
) and its value is the current value ofi
in the environment object of first iteration (iter1EnvObj
) which is 1 in this case.Value of the variable
i
in the current environment object (iter2EnvObj
) is then incremented from 1 to 2 as a result of execution of increment part (i++
) of thefor
loop.After incrementing the value of
i
, loop's condition (i <= 3
) is checked. Since it is still true, loop's body is executed for the second time. As in step 7, callback function for thesetTimeout
is created, reference to the environment object of second loop iteration (iter2EnvObj
) is saved in the internal [[Environment]] slot of the callback function. After that, callback function is passed to thesetTimeout
function as an argument.
At this point, following is a simplified diagram of how each environment is linked to each other:
Steps from 8 to 11 are repeated for third iteration of the loop. Following diagram shows the linkage between the environments after the third iteration:
As you can see in the above diagrams, each time setTimeout
is called, its callback function closes over a different environment object created for that particular iteration of the loop and this environment object has its own copy of the loop variable i
.
As a result, when the callback functions of each setTimeout
call are called, output is 1, 2, 3 instead 4, 4, 4. This is because each callback function closed over a different environment object with its own copy of variable i
.
Summary
Using let
solved the "Closures in Loop" problem because each iteration got its own environment object with its own copy of the loop variable and each timer function closed over a different loop variable instead of the same one. As a result, each timer function only logged the value of i
that it closed over, irrespective of what the value of i
became at the end of the loop.