Scope, hoisting, event loop: An issue with `var` variables and loops

There is one interesting thing about loops and var that might cause a few headaches to developers, and I think this will help clarify a lot of what’s going on with scope.

Take this example:

const operations = []

for (var i = 0; i < 5; i++) {
  operations.push(() => {
    console.log(i)
  })
}

for (const operation of operations) {
  operation()
}

It basically iterates and for 5 times it adds a function to an array called operations. This function console logs the loop index variable i.

Later it runs these functions.

The expected result here should be:

0
1
2
3
4

but actually what happens is this:

5
5
5
5
5

Why is this the case? Because of the use of var.

The above code equals to

var i;
const operations = []

for (i = 0; i < 5; i++) {
  operations.push(() => {
    console.log(i)
  })
}

for (const operation of operations) {
  operation()
}

so, in the for-of loop, i is still visible, it’s equal to 5 and every reference to i in the function is going to use this value.

So how should we do to make things work as we want?

The simplest solution is to use let declarations for the loop variables.

They are a great help in avoiding some of the weird things about var declarations.

Changing var to let in the loop variable is going to work fine:

const operations = []

for (let i = 0; i < 5; i++) {
  operations.push(() => {
    console.log(i)
  })
}

for (const operation of operations) {
  operation()
}

Here’s the output:

0
1
2
3
4

This works because on every loop iteration i is created as a new variable each time, and every function added to the operations array gets its own copy of i.

Keep in mind you cannot use const in this case, because there would be an error as for tries to assign a new value in the second iteration.

Another way to solve this problem that was very common, when we didn’t have let, was to use an Immediately Invoked Function.

In this case you’d wrap the entire function and bind i to it. Since in this way you’re creating a function that immediately executes, you return a new function from it, so we can execute it later:

const operations = []

for (var i = 0; i < 5; i++) {
  operations.push(((j) => {
    return () => console.log(j)
  })(i))
}

for (const operation of operations) {
  operation()
}

Lessons in this unit:

0: Introduction
1: Global scope
2: Function scope
3: Block scope
4: Shadowing
5: Hoisting
6: Closures
7: ▶︎ An issue with `var` variables and loops
8: The event loop
Are you intimidated by Git? Can’t figure out merge vs rebase? Are you afraid of screwing up something any time you have to do something in Git? Do you rely on ChatGPT or random people’s answer on StackOverflow to fix your problems? Your coworkers are tired of explaining Git to you all the time? Git is something we all need to use, but few of us really master it. I created this course to improve your Git (and GitHub) knowledge at a radical level. Launching May 21, 2024. Join the waiting list!