Friday, February 23, 2018

Javascript variables: var vs let vs const

Yesterday, a colleague of mine asked me what the difference between let and var is. This post will clarify some things, hopefully for other people too.

The let keyword is a new keyword for declaring variables and it works differently than var. To help demonstrate the difference, let’s quickly review two characteristics of var and JavaScript: hoisting and functional scope.

Var in JavaScript works a bit differently than similar keywords in other languages. For example, let’s consider this JavaScript function:
If the ‘hour’ parameter is less than 18 it appears that we are declaring variable called ‘greeting’ (line of code: 3) and if the ‘hour’ parameter is greater or equal to 18, it looks like we are declaring a different variable called ‘greeting’ (line of code: 5). This may be an issue in other languages, but in JavaScript, these two ‘greeting’ variables are actually the same. Anytime a var is used in JavaScript, something called hoisting happens. Behind the scenes, the JavaScript code is rewritten so that all variable declarations using the var keyword in a function are hoisted to the top of that function. This means that in reality, the code we wrote looks like this when it executes:
One declaration of ‘greeting’ is hoisted to the top of the function and any other declarations of ‘greeting’ are ignored. Because the var keywords are ignored after hoisting happens, lines of code four and six are normal assignments. We can prove this behavior, with doing something really unusual. Let’s move the declaration of ‘greeting’ down to the bottom of the function. I don’t recommend doing this because it’s really confusing, but because of hoisting this code will still work as it’s expected, even it looks like that the declaration of ‘greeting’ is blocked by the return statement and will never be reached.

The next important thing to understand about var is scope. The var keyword uses a functional scope. We already saw that the uses of the var keyword inside a function get hoisted to the top of that function. Uses of var outside of a function are considered part of the global scope and get hoisted to the “top” of the global scope. This global scope is shared even across different scripts and redeclaring the global variable with var has no effect for the same reason that redeclaring a function’s variable with var has no effect. The declarations are essentially consolidated when they are hoisted. For example, if we have script1.js and script2.js and both declare a variable called ‘person’ using the var keyword, JavaScript treats this as a single variable in the global scope. The value of person at any given time depends on the order in which these scripts are referenced. So, this isn’t anything new, it’s how var is working in JavaScript all long.

Now, that we reviewed var keyword, let’s talk about the new ES6 keyword: let. As we said, var hoists and uses function scope. The new ES6 keyword let is different than var because variables declared with let are not hoisted and they use block scope. Well, let’s go back to the original example, but change each var keyword to let. Now TypeScript gives us an error “at line 7, Cannot find name ‘greeting'”:


Firstly, the let keyword in ES6 has block-scope. Blocks can generally be thought of as closest containing a set of curly braces. This will be familiar to those, who also know languages like C, CSharp or Java. With let, the first ‘greeting’ (line of code: 3) is a variable that has only scope to the if-statement curly braces and the second ‘greeting’ (line of code: 5) is variable that has only scope to the else-statement curly braces. Because it has this branch scope, the final declaration of ‘greeting’ in the return statement doesn’t reference anything at all, which is why TypeScript gives us an error. So, what the var keyword gives us function scope, the let keyword gives us block scope. What happens if we fix this by adding a new line to declare the variable at the top of the function?

Now, this is even worse, because we’ve got three versions of ‘greeting’, the one we just made in the block scope of the function and a separate one in each of the if-branches. As written, this function will always return undefined, because the values are only set on the variable scope to the branches, not the one declared at the top of the function. To get the story correctly, we need to remove the let keyword in each branch:
Now, there is one ‘greeting’ variable that exists throughout the function and it will work as intended. The second major difference from var is that the let keyword doesn’t hoist. Try to declare the let keyword after the return statement and you will get an error: “block-scoped variable ‘greeting’ used before its declaration”. The reason we are getting this error is that variables declared with let are not hoisted. Because of hosting, it’s ok to redeclare a variable with the var keyword inside a scope. The var keywords are hoisted to the top of function scope. However, with let redeclaring a variable on the same scope becomes a duplicate identifier error. This could be useful to identify possible bugs in your program.

Using let in a for-loop

A special case that I want to highlight when let keyword bends the rules of block scoping and that’s when let is used to declare the iterator variable in a for-loop. Let’s consider the following function called printNumbers:

It's designed to naively log out numbers from one to whatever number is passed in as the max parameter. What happens if we add a line of code that consoles ‘i’ outside the loop?
If we didn’t understand hoisting and functional scope in JavaScript, you might expect this would be an error. However, now we know, when var is used, the declaration of ‘i’ is hoisted to the top of the function. So, ‘i’ remains in scope throughout the entire function. If we call print numbers with a max of 10, the extra console.log we added (line code: 5) will log 11. That’s the first number that met the for-loop exit condition. For-loop iterators declared with var, are not reset when the loop exits.

If we were duplicated the loop and reuse ‘i’ iterator, our code now uses the same ‘i’ variable in both loops:
The variable ‘i’ is reset at the top of each loop so the iteration will work as expected. But if we were using ‘i’ for something else inside this function, its previous value would be lost. The lines with var are treated as an assignment to the same function’s scoped ‘i’ variable in both loops, they don’t create their own variable.

Let’s switch these var keywords to let. As we said before, ES6 slightly bends the rules of block scoping, when let is used to initialize a for-loop:
Even if it’s technically outside the curly braces, the iterator of the for-loop declared with let is scoped to be inside the braces. With this code, a separate ‘i’ variable is now scoped within each for-loop only. No ‘i’ variable exists in the middle anymore. That’s why TypeScript shows an error for our extra console.log(i) statement (line of code: 5):

Scope per iteration behavior of let

ES6 has another very interesting behavior when let is used to initialize the for-loop, beyond just keeping the iterator scope to the loop’s curly braces. Using let creates a separate copy of the variable for each iteration through the for-loop. To demonstrate why this is useful, let’s take a look at this example:
First, we declare the for-loop using the var keyword and later, the handler function for the button onclick event is created. If we run the example, every click on the button is going to alert the same message: “This is button 4”. The base of what we know about the var keyword, it’s not surprising that the ‘i’ declared in the click handler is the same ‘i’ declared in the for-loop. However, what it’s often surprising is that each iteration through the for-loop refers to the same ‘i’ variable. A copy of its value is never made and so each click handler alerts the same message, for the first value of ‘i’ that met the for-loop exit condition and that value is preserved in the closed scope shared by each click handler. So, how we fix this?  To get this to work as intended in ES5 JavaScript, we have to manually create a new scope that will close over the value of ‘i’ in each iteration. That way, each handler can display the appropriate message. A common way is to wrap the code that creates the click handler in an Immediately-Invoked Function Expression or IIFE:
So, now each run through the loop will have a separate function that retains access to a separate closed over scope. That includes a different value for ‘buttonNumber’ variable. If we run the example again, we can see that the code works as expected. However, that seems like a lot of work. In ES6, we can just use the let keyword to declare our variable in the for-loop and this behavior happens automatically for us. When using let within a for-loop, each iteration gets a separate copy of the iterator variable and its own scope, which eliminates the need of IIFE:


Introduction to const

The const keyword in ES6 is very similar to let. Variables declared with const are also blocked scoped and aren’t hoisted. The main difference is that the value of a const must be set when it’s declared and cannot be changed later in the scope. In ES6, you have to provide the value for a const, when it’s declared:

const myvar;


Const in ES6 behaves a bit differently than in some other languages though. For example, an ES6 const can’t be instantiated to return value of a function or an expression. Also, it doesn’t have to be a true constant, like a string literal or a number. This means that code like this:

const startTime = Date.now();
const answer = confirm('Are you sure?');

works perfectly fine. Const effectively prevents reassigning a new value, even when it’s used to declare an object. Let’s create an object literal:

const simpleLiteral = {
  myProperty: 1
};

If we try to set the ‘simpleLiteral’ to a new value, TypeScipt shows us an error:

const simpleLiteral = {
  myProperty: 1
};

simpleLiteral = {
  myProperty: 1
}


However, if we try to set the value of a property of ‘simpleLiteral’, this works fine:

const simpleLiteral = {
  myProperty: 1
};

simpleLiteral.myProperty = 2;

So, it’s important to remember that the value of an ES6 const itself must remain constant, but the value of any of its properties may be changed. Does that mean that there is no way to have properties of an object being constant via the const keyword? Well, it’s possible to be achieved using TypeScript namespaces. A namespace is a new keyword that was introduced in TypeScript version 1.5. It replaces the uses of the module keyword. Namespaces are TypeScript only concept and not part of the ES6 standard. Let’s do a quick test of how properties declared with const working in TypeScript namespace:

namespace AtomicNumbers {
  export const H = 1;
  export const He = 2;
};

The export keyword means that these properties are available outside of the namespace keyword. If we try to redeclare some of the properties, we will get an error that we cannot redeclare block-scoped variable:

namespace AtomicNumbers {
  export const H = 1;
  export const He = 2;
};

AtomicNumbers.H = 3;

We get this error, even if we try to modify the namespace from outside.

When to use let and const?

So, when should we actually use let and const with ES6 and TypeScript 1.8? I think that using the new keywords where appropriate can help to clarify the intended purpose and scope of the variables. As a bonus, it’s also nice to eliminate the hand-coding IIFEs in for-loops. If we are looking for a good recommendation, I think a fair one is that first, we should try to use var, when you intend for variables to be resistant to the declaration. Mostly this will be for intentionally global variables that are used across files. That scenario should be pretty rare. The second case, when you may still wish to use var, is inside a function if the block scoping behavior of let and const creates an awkward situation. For an example, try-catch block. Pretty much, we should use let and const at all other times. The tighter scope of let and const declared variables will hopefully lead to more correct programs that are easier to understand. In addition, we should try to use const over let as much as possible. You will be surprised how often variables don’t need to be updated after being initialized, particularly object literals (don’t forget, that we can update the properties of a const). A cool thing about using TypeScript is that can tell you instantly of a variable you declare with const if it’s ever used in left-hand side of an expression. For these variables, you just need to use let instead.