February 10, 2018 · Javascript ES6

ES6 `let` vs `var`

This post is the start of a new series that will cover new language features introduced in ECMAScript 6. Even though ECMAScript 6 has been around for a while now, you may still not fully understand the subtleties behind the new language features. The approach I'm going to take is to explain these concepts by first describing how things used to be done in ES5. This will help to motivate why the new ES6 language features were created and what problems they were supposed to solve.

You may be thinking, "Why should I even bother learning about how things used to be written? That was then, and ES6 is the future!". Well, even though ES6 is the cool new thing, you're still going to see a lot of ES5 code. This is especially true if you need to deal with legacy code. Or you may run into ES5 if you're looking at the source code of an open source project from a few years back. Plus, understanding things from this point of view will help you to gain a deeper understanding of Javascript in general.

So without any further ado, let's jump right into it!

The Problem

Do you remember when you first learned Javascript and you ran into code like this?:

if (true) {
  var functionScoped = 999999;
}

Or how about like this?:

for (var i = 0; i < 10; i++) {
  console.log("do something 10 times");
}

I sure do. I remember assuming that the curly braces would create a brand new scope. So both functionScoped and i would only be accessible within their respective blocks.

That's an intuitive assumption right? But I quickly learned otherwise. The curly braces don't actually create any new scopes at all. In fact, they have no effect whatsoever in terms of variable scope.

if (true) {
  var functionScoped = 999999;
}

for (var i = 0; i < 10; i++) {
  console.log("do something 10 times");
}

console.log(functionScoped); // prints 999999
console.log(i); // prints 10

In the code above, functionScoped and i are both accessible from the global scope. This is because var declares variables that are function scoped. And if there is no enclosing function where var is declared, then var declares a variable in the global scope.

Take a look at the following code:

function createFunctionScope() {
  if (true) {
    var functionScoped = 999999;
  }
}

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

Now functionScoped isn't accessible from the global scope. That's because functionScoped is enclosed within a function now. The function basically creates a brand new scope that "traps" the variable and refuses to let it leak out into any outer scope!

So basically this was just something you had to commit to memory:

var creates variables that "leak" through blocks attached to conditionals and loops. The only thing that var doesn't leak through is functions.

How We Dealed in ES5

Have you ever seen code that looks like this?:

(function() {
  for (var i = 0; i < 10; i++) {
    console.log("do something 10 times");
  }
})();

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

This was a way for developers to declare variables inside blocks, while at the same time not having those variables leak through to the global scope. The technique is called IIFE, or immediately invoked function execution. The basic idea is to wrap the block of code inside an anonymous function. Now the variables are "trapped" inside the function scope. Then we enclose the anonymous function inside parentheses () so that we can invoke it immediately. The basic template looks like this: (function() {})();.

With IIFEs, the code gets executed immediately just like it normally would. But now, the variables that would normally leak through to the global scope are now enclosed within the newly created function scope. Using IIFEs was a really common practice, since it's bad practice to pollute the global namespace.

Enter let

With the advent of let in ES6 though, we don't need to use IIFEs anymore! let always creates variables that are block scoped. For instance, what do you think the following code will print?

if (true) {
  let blockScoped = 777777;
}

for (let i = 0; i < 10; i++) {
  console.log("do something 10 times");
}

console.log(blockScoped);
console.log(i);

No, that wasn't a trick question! let does exactly what you think it would do. It creates variables that are scoped to the enclosing block. So blockScoped and i will not be accessible from the outer scope.

if (true) {
  let blockScoped = 777777;
}

for (let i = 0; i < 10; i++) {
  console.log("do something 10 times");
}

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

This is great! In fact, many people think that var is pretty much obsolete now, and I agree. Now we can simply declare all our variables with either const or let. Doing this will help avoid many potential sources of confusion.

What about const?

Actually, const behaves in the exact same way as let. That is, variables declared with const are block scoped too. The only thing is that const prevents you from re-assigning the value of the variable. Hence the name, const, which is short for constant. const is really useful and should be used whenever possible, since it provides some safeguards against the problems that can occur with mutating state.

I specifically said some safeguards, because const doesn't prevent mutation of state per se. For instance, if I assign a variable declared with const to an array/object, I can still mutate the values within the array/object. I just can't re-assign that constant to a different value.

That's it!

I hope that clears up the differences between let and var. It's worth gaining mastery over this topic, since we use these language features all the time when we write Javascript code! Next time, we'll jump into talking about ES6 arrow functions.