/ coding

ES5 vs ES6: Solving the classic `for` loop problem

There’s a classic problem JavaScript programmers are sure to come across at some point where we need to console log the i in a for loop at a later time.

Let’s look at the problem, solutions in ES5, and then the remarkably simple solution in ES6.

The problem

Again, we are trying to console log i at some point in the future. So while the code below will work as expected, it is not a solution to the problem:

for (var i = 0; i < 10; i++) {
  console.log(i);
}
// Outputs 0, 1, 2, 3, 4, 5, 6, 7, 8, 9

It outputs the correct numbers because we are console logging as we go, not at a later time.

Below are two approaches to console logging i at a later time, both of which produce incorrect output.

With setTimeout

for (var i = 0; i < 10; i++) {
    setTimeout(function () {
        console.log(i);
    }, 0);
}
// Outputs 10, 10, 10, 10, 10, 10, 10, 10, 10, 10

By pushing functions to an array and calling them later

var arr = [];
for (var i = 0; i < 10; i++) {
  arr.push(function() {
    console.log(i);
  });
}

arr.forEach(function(e) {
  e();
});
// Outputs 10, 10, 10, 10, 10, 10, 10, 10, 10, 10

Why don’t these solutions work?

In both solutions, the console log is happening after the for loop has completed, which is what we want. But if the for loop has already completed when we console log, what is the value of i?

It’s 10.

Why is i equal to 10?

Remember that JavaScript doesn’t have block scoping of var variables. var variables are function scoped. This means that var i belongs to the current scope, which must either be the current function we are inside of, or in the cases above, the global or window scope.

So when the for loop’s i++ increments i, it is changing the value for i at a higher level scope than you may have expected. When we console log i, there is only one i out there to console log.

Since we have waited until the for loop has completed, i has already been incremented 10 times, and we end up console logging 10 every time we console log i.

What do we need to solve this problem?

In both cases, we need a lower level scope that captures the value of i for each iteration of the for loop.

Note: If you’re interested in trying to solve this problem yourself, now would be the time to pause your reading.

Now let’s look at ways to solve this problem in both ES5 and ES6.

Using ES5

We need a lower level scope to contain the value of i within each iteration of the for loop. The only way to create a scope in ES5 is to make a function, so we’ll need to nest a function inside of the for loop.

Since we want the nested function to run automatically on each iteration, we will use an IIFE (immediately invoked function expression). If you aren’t familiar with IIFEs, it would be a good idea to look into those before continuing, but the basic syntax is:

(function () {})();

Solutions in ES5

With setTimeout

for (var i = 0; i < 10; i++) {
    (function(j) {
      setTimeout(function () {
          console.log(j);
      }, 0)
    })(i);
}
// Outputs 0, 1, 2, 3, 4, 5, 6, 7, 8, 9

console.log(i); // 10
console.log(j); // ReferenceError: j is not defined

By pushing functions to an array and calling them later

var arr = [];
for (var i = 0; i < 10; i++) {
  (function(j) {
    arr.push(function() {
      console.log(j);
    });
  })(i);
}

arr.forEach(function(e) {
  e();
});

console.log(i); // 10
console.log(j); // ReferenceError: j is not defined

Why do these ES5 solutions work?

In short, because we are using functions to create scopes which allow us to store local values for each function that are independent of values in other scopes.

So what does all of that mean? Let’s break it down.

On the first iteration, i is 0. i’s value is passed, via our IIFE and stored as j. j is a local variable to this specific function for this specific iteration of the loop. This particular j will not be interfered with by js created in further loop iterations because they exist in scopes of their own.

Visually, it is something like this:
image

So with the above illustration in mind, each locally scoped j can be its own unique value without interfering with the other js.

Further, each local scope has access to i which is on the top-level scope. But since i only exists once, if its value is changed, all local scopes only see the new value (this was the source of our original problem).

To recap, in ES5, we create a local scope to store a local value by creating a wrapper function, since functions are the only way to create a new scope in ES5.

Using ES6

ES6 gives us a new way to scope values: the let statement.

let statements declare a block scope local variable. In other words, we don’t need a wrapper function to create a new scope.

As described by MDN:

let allows you to declare variables that are limited in scope to the block, statement, or expression on which it is used. This is unlike the var keyword, which defines a variable globally, or locally to an entire function regardless of block scope.

let statements make solving our problem quite trivial: simply change var to let.

Solutions in ES5

With setTimeout

for (let i = 0; i < 10; i++) {
      setTimeout(function () {
          console.log(i);
      }, 0);
}
// Outputs 0, 1, 2, 3, 4, 5, 6, 7, 8, 9

console.log(i); // ReferenceError: i is not defined

By pushing functions to an array and calling them later

var arr = [];
for (let i = 0; i < 10; i++) {
  arr.push(function() {
    console.log(i);
  });
}

arr.forEach(function(e) {
  e();
});

console.log(i); // ReferenceError: i is not defined

Why do these ES6 solutions work?

In ES6, the let statement creates a new scope, just like our functions do in ES5. Each iteration of the for loop creates a new locally scoped i which is not overwritten by the following locally scoped is, similar to how our j variables work in the ES5 solutions.

Another nicety here (as demonstrated by our console.log(i); statements in the code above) is that we don’t have to create a variable at our top level scope just to do a for loop. By using let i, the i variable never exists at the top level. Since our top level scope doesn’t need i for anything, it’s great that we can keep it contained locally by using let.

Learn it both ways

Using let in ES6 certainly feels like a nice, concise solution, and it’s a good idea to start learning about what ES6 offers as soon as possible since the release date seems to be set to happen sometime very soon.

But we still live in a world where ES5 is the only standard in use by web browsers and most existing applications are written in ES5 only.

If you want to start writing your app in ES6 today, check out Babel, which will transpile your ES6 code into ES5 JavaScript. In fact, Meaniscule uses Babel to transpile your application code if you want to write in ES6.

Want more let?

Managing scope in for loops is far from the only use for let. For example, here is a let variable scoped to an if block:

var a = 5;

if (a < 10) {
  let a = 10;
  let b = true;
  console.log(a); // 10
  console.log(b); // true
}

console.log(a); // 5
console.log(b); // ReferenceError: b is not defined

Notice how:
a inside of the if block logs a different value than a outside of the block
b inside of the if block logs a value and b outside of the block throws a ReferenceError because it doesn’t exist in the outside scope

For more on let, here’s a great post by David Walsh, which covers further use cases and potential pitfalls (e.g., unlike var, let isn’t hoisted).