The Perils of Function Scoping

As you probably know, block-scoping using let is being introduced in the ECMAScript 6 standard. But why is this even neccesary? A lot of people seem confused about this.

Should we go ahead and replace every occurence of var with let now?


Let’s analyze the situation.

The Problem

One of the problems with function scoping is that you don’t automatically fall into the pit of success when you are dealing with asynchronous function calls.

Let’s say we have an array of filenames and we want to do some asynchronous operation in a node.js environment with them.

var files = [
  'Photo 1.jpg',
  'Photo 2.jpg',
  'Photo 3.jpg'
];

In this example we are interested in the result of fs.stat for each of those files. So we iterate through the array using a for loop and call the function with a callback:

for (var i = 0; i < files.length; ++i) {
  var file = files[i];

  fs.stat(file, function callback(err, stats) {
    console.log(file);
  });
}

And here comes the output of this fabulous script:

Photo 3.jpg
Photo 3.jpg
Photo 3.jpg

Oops, how did that happen? The callback function keeps a closure of the file variable and then the value of that one variable is reassigned on every iteration of the loop. So the problem is, that we have three closures of the same variable instead of three closures to separate variables.

A Possible Solution: let

Luckily we heard about this crazy new thing in JavaScript called let which gives us block-scoping:

for (var i = 0; i < files.length; ++i) {
  let file = files[i];
// ^--- the only change
  fs.stat(file, function callback(err, stats) {
    console.log(file);
  });
}

Now the iteration works as expected:

Photo 1.jpg
Photo 2.jpg
Photo 3.jpg

Another Solution: Array.prototype.forEach

On first sight, using a for loop and and a Array.prototype.forEach construct for array iteration might seem identical in behavior, but they actually differ slightly: The forEach approach gives you a new scope on every iteration. Whereas an ordinary for loop takes place inside of the same scope during the complete iteration.

files.forEach(function(file) {
  fs.stat(file, function callback(err, stats) {
    console.log(file);
  });
});

And we get the correct output as well.

So, should I use let for everything now?

First of all, not all JavaScript runtimes support let yet (node.js, IE<11 and Safari are the biggest blockers currently), so you have to use transpilation which comes with a small cost.

With the Babel transpiler (v5.4.3) the first solution from above is translated to a function that is invoked on every iteration:

var _loop = function () {
  var file = files[i];
  fs.stat(file, function callback(err, stats) {
    console.log(file);
  });
};

for (var i = 0; i < files.length; ++i) {
  _loop();
}

This is a great solution performance-wise and, as a consequence, there is no reason to not replace every instance of var with let even in large codebases if you are willing to use Babel or you are only targeting environments like io.js or Firefox/Chrome/IE11 which support the let statement natively.