let’
solves a common JavaScript “gotcha”…
I’ve been writing JavaScript using the ES2015 (ES6) syntax for awhile now and have grown to appreciate many of the language changes for their elegance and simplicity. One of the first and easiest changes I adapted to was using let
/const
instead of var
. However, I took for granted the benefit let
provides over var
; it isn’t just a flashy new syntax for var
, but rather provides an important scoping mechanism.
I took for granted the benefit
let
provides overvar...
It’s worth noting first that the vast majority of variables I declare are best suited for using _const_
instead of let
/var
. This is because const
will throw an error if an attempt is made to change its value after it has been declared, a useful feature to prevent accidental mutation. However, a mutable variable is often needed, particularly as a counter in looping scenarios. But why should you use let
instead of var
for these situations, if they both provide the basic mutability required of them?
The simple answer is that **let**
provides block-scoping that is absent in the function-scoped var
. This explanation obviously begs a practical example. Allow me to demonstrate this important distinction using a classic front-end engineering interview question:
In this example, what will be printed in the console?
var callbacks = [];(function() {for (var i = 0; i < 5; i++) {callbacks.push( function() { return i; } );}})();
console.log(callbacks.map( function(cb) { return cb(); } ));
In this example, we loop 5 times, each time pushing a function into the callback
array. After the array is filled with 5 functions, we run each of them, logging their result to the console. A novice engineer might answer (incorrectly) that the result is [0, 1, 2, 3, 4]
, a reasonable analysis, but one that falls victim to the JavaScript “hoisting” gotcha.
Don’t be a victim. Avoid the hoisting trap with ‘let’!
The correct answer is actually [5, 5, 5, 5, 5]
, and makes sense when you consider what hoisting does behind the scenes:
var callbacks = [];(function() {var i;for (i = 0; i < 5; i++) {callbacks.push( function() { return i; } );}})();
console.log(callbacks.map( function(cb) { return cb(); } ));
Notice how JavaScript hoists the variable declaration to the top of the function block, causing the value of i
to be 5
by the time each of the callback functions have executed, since the for
loop has incremented completely before any of these functions are called.
There are a number of ways to classically solve this problem and get the script to log [0, 1, 2, 3, 4]
to the console, but let
gives us a very simple solution:
var callbacks = [];(function() {for (let i = 0; i < 5; i++) {callbacks.push( function() { return i; } );}})();
console.log(callbacks.map( function(cb) { return cb(); } ));
That’s it — just replace **var**
with **let**
and the example works as expected! This is thanks to the block-scoping behavior of let
. Rather than being hoisted to the top of the function’s scope, _let_
stays in the block scope of the loop, causing a separate instance of i
for each iteration.
So, should you ever use var
? As you have likely gathered from the title of this article, I am of the opinion that you should never use **var**
. Technically, var
is still useful in situations where you want to maintain function scope instead of block scope, but I am of the firm belief that if you need to rely on an unintuitive hoist to get your script to work, you have bigger problems 😜.
Update: In response to two of the most common points of discussion readers are bringing up:
const
isn’t truly immutable. For instance:
const myNotQuiteImmutableObject = {thisCanBeChanged: "not immutable"};
myNotQuiteImmutableObject.thisCanBeChanged = "see I changed it.";
However, this still prevents basic mutation like:
const immutableString = "you can't change me";
immutableString = "D'OH!"; // error
If you need real immutability, check out Facebook’s excellent Immutable library.
2. “let
isn’t supported in {mySuperAncientBrowser}
.” This is true, and even somewhat recent browsers still don’t have support for [let](http://caniuse.com/#feat=let)
. This has an obvious and easy solution: Use Babel. Babel will allow you to use all the best and latest features of JavaScript and then transpile into a vocabulary that even simple old IE8 can understand (with some exceptions).