Understanding "Closures in Loop" Problem and How it is Solved in ES6

Understanding "Closures in Loop" Problem and How it is Solved in ES6

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:

  1. When foo function is called, new environment object (let's call it EnvObj1) gets created.

  2. EnvObj1 environment object is linked to the environment that is saved in the internal [[Environment]] slot of the foo function which, in this case, is the global environment.

  3. Execution of the for loop is started by remembering the list of variables declared with let in the initialization part of the for 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.

  4. A new environment object (let's call it initEnvObj) is created for the initialization part of the loop and a binding for the variable i is created in this environment object. This environment gets environment object EnvObj1 (created in step 1) as its outer environment.

  5. A new environment (iter1EnvObj) is created for the first iteration of the loop and EnvObj1 (created in step 1) is set as the outer environment of this newly created environment: iterEnvObj.

  6. Referring to the environment created in step 3, a new binding for the variable i is created in the iter1EnvObj, setting its initial value equal to the value of the variable i in the environment object (initEnvObj) that was created in step 4.

  7. Because the loop condition i <= 3 is true, body of the loop is executed, creating a callback function for the setTimeout, 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 the setTimeout function as an argument.

    Following is the simplified diagram of how each environment is linked to its outer environment at this point: Screenshot 2020-12-26 at 9.35.23 PM.png

    At this point, first iteration of the loop is done. Now, we are ready for the second iteration of the loop.

  8. Second iteration starts with the creation of a new environment object (let's call it iter2EnvObj) and envObj1 (created in step 1) is set as its outer environment.

  9. Using the environment object initEnvObj (created in step 3), a new binding for variable i is created in the environment object created for the second iteration of the loop (iter2EnvObj) and its value is the current value of i in the environment object of first iteration (iter1EnvObj) which is 1 in this case.

  10. 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 the for loop.

  11. 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 the setTimeout 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 the setTimeout function as an argument.

At this point, following is a simplified diagram of how each environment is linked to each other:

Screenshot 2020-12-26 at 9.12.47 PM.png

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:

Screenshot 2020-12-26 at 9.18.52 PM.png

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.

Did you find this article valuable?

Support Yousaf Khan by becoming a sponsor. Any amount is appreciated!