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:
for (var i = 0; i < 10; i++) {
process(items[i]);
}
// i is still accessible here
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:
for (let i = 0; i < 10; i++) {
process(items[i]);
}
// i is not accessible here - throws an error
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:
var funcs = [];
for (var i = 0; i < 10; i++) {
funcs.push(function() { console.log(i); });
}
funcs.forEach(function(func) {
func(); // outputs the number "10" ten times
});
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:
var funcs = [];
for (var i = 0; i < 10; i++) {
funcs.push((function(value) {
return function() {
console.log(value);
}
}(i)));
}
funcs.forEach(function(func) {
func(); // outputs 0, then 1, then 2, up to 9
});
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:
var funcs = [];
for (let i = 0; i < 10; i++) {
funcs.push(function() {
console.log(i);
});
}
funcs.forEach(function(func) {
func(); // outputs 0, then 1, then 2, up to 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:
var funcs = [],
object = {
a: true,
b: true,
c: true
};
for (let key in object) {
funcs.push(function() {
console.log(key);
});
}
funcs.forEach(function(func) {
func(); // outputs "a", then "b", then "c"
});
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:
var funcs = [];
// throws an error after one iteration
for (const i = 0; i < 10; i++) {
funcs.push(function() {
console.log(i);
});
}
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:
var funcs = [],
object = {
a: true,
b: true,
c: true
};
// doesn't cause an error
for (const key in object) {
funcs.push(function() {
console.log(key);
});
}
funcs.forEach(function(func) {
func(); // outputs "a", then "b", then "c"
});
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
).