Block Binding in Loops

Perhaps one area where developers most want block level scoping of variables is within for loops, where the throwaway counter variable is meant to be used only inside the loop. For instance, it’s not uncommon to see code like this in JavaScript:

  1. for (var i = 0; i < 10; i++) {
  2. process(items[i]);
  3. }
  4. // i is still accessible here
  5. console.log(i); // 10

In other languages, where block level scoping is the default, this example should work as intended, and only the for loop should have access to the i variable. In JavaScript, however, the variable i is still accessible after the loop is completed because the var declaration gets hoisted. Using let instead, as in the following code, should give the intended behavior:

  1. for (let i = 0; i < 10; i++) {
  2. process(items[i]);
  3. }
  4. // i is not accessible here - throws an error
  5. console.log(i);

In this example, the variable i only exists within the for loop. Once the loop is complete, the variable is no longer accessible elsewhere.

Functions in Loops

The characteristics of var have long made creating functions inside of loops problematic, because the loop variables are accessible from outside the scope of the loop. Consider the following code:

  1. var funcs = [];
  2. for (var i = 0; i < 10; i++) {
  3. funcs.push(function() { console.log(i); });
  4. }
  5. funcs.forEach(function(func) {
  6. func(); // outputs the number "10" ten times
  7. });

You might ordinarily expect this code to print the numbers 0 to 9, but it outputs the number 10 ten times in a row. That’s because i is shared across each iteration of the loop, meaning the functions created inside the loop all hold a reference to the same variable. The variable i has a value of 10 once the loop completes, and so when console.log(i) is called, that value prints each time.

To fix this problem, developers use immediately-invoked function expressions (IIFEs) inside of loops to force a new copy of the variable they want to iterate over to be created, as in this example:

  1. var funcs = [];
  2. for (var i = 0; i < 10; i++) {
  3. funcs.push((function(value) {
  4. return function() {
  5. console.log(value);
  6. }
  7. }(i)));
  8. }
  9. funcs.forEach(function(func) {
  10. func(); // outputs 0, then 1, then 2, up to 9
  11. });

This version uses an IIFE inside of the loop. The i variable is passed to the IIFE, which creates its own copy and stores it as value. This is the value used by the function for that iteration, so calling each function returns the expected value as the loop counts up from 0 to 9. Fortunately, block-level binding with let and const in ECMAScript 6 can simplify this loop for you.

Let Declarations in Loops

A let declaration simplifies loops by effectively mimicking what the IIFE does in the previous example. On each iteration, the loop creates a new variable and initializes it to the value of the variable with the same name from the previous iteration. That means you can omit the IIFE altogether and get the results you expect, like this:

  1. var funcs = [];
  2. for (let i = 0; i < 10; i++) {
  3. funcs.push(function() {
  4. console.log(i);
  5. });
  6. }
  7. funcs.forEach(function(func) {
  8. func(); // outputs 0, then 1, then 2, up to 9
  9. })

This loop works exactly like the loop that used var and an IIFE but is, arguably, cleaner. The let declaration creates a new variable i each time through the loop, so each function created inside the loop gets its own copy of i. Each copy of i has the value it was assigned at the beginning of the loop iteration in which it was created. The same is true for for-in and for-of loops, as shown here:

  1. var funcs = [],
  2. object = {
  3. a: true,
  4. b: true,
  5. c: true
  6. };
  7. for (let key in object) {
  8. funcs.push(function() {
  9. console.log(key);
  10. });
  11. }
  12. funcs.forEach(function(func) {
  13. func(); // outputs "a", then "b", then "c"
  14. });

In this example, the for-in loop shows the same behavior as the for loop. Each time through the loop, a new key binding is created, and so each function has its own copy of the key variable. The result is that each function outputs a different value. If var were used to declare key, all functions would output "c".

I> It’s important to understand that the behavior of let declarations in loops is a specially-defined behavior in the specification and is not necessarily related to the non-hoisting characteristics of let. In fact, early implementations of let did not have this behavior, as it was added later on in the process.

Constant Declarations in Loops

The ECMAScript 6 specification doesn’t explicitly disallow const declarations in loops; however, there are different behaviors based on the type of loop you’re using. For a normal for loop, you can use const in the initializer, but the loop will throw a warning if you attempt to change the value. For example:

  1. var funcs = [];
  2. // throws an error after one iteration
  3. for (const i = 0; i < 10; i++) {
  4. funcs.push(function() {
  5. console.log(i);
  6. });
  7. }

In this code, the i variable is declared as a constant. The first iteration of the loop, where i is 0, executes successfully. An error is thrown when i++ executes because it’s attempting to modify a constant. As such, you can only use const to declare a variable in the loop initializer if you are not modifying that variable.

When used in a for-in or for-of loop, on the other hand, a const variable behaves the same as a let variable. So the following should not cause an error:

  1. var funcs = [],
  2. object = {
  3. a: true,
  4. b: true,
  5. c: true
  6. };
  7. // doesn't cause an error
  8. for (const key in object) {
  9. funcs.push(function() {
  10. console.log(key);
  11. });
  12. }
  13. funcs.forEach(function(func) {
  14. func(); // outputs "a", then "b", then "c"
  15. });

This code functions almost exactly the same as the second example in the “Let Declarations in Loops” section. The only difference is that the value of key cannot be changed inside the loop. The for-in and for-of loops work with const because the loop initializer creates a new binding on each iteration through the loop rather than attempting to modify the value of an existing binding (as was the case with the previous example using for instead of for-in).