Javascript: var, const, let, What’s The Difference?

In The Beginning, There Was var

Not so long ago, back in the days of ES5, there was only one keyword used to define a variable: var. Var has two possible types of scope: function scope and global scope. The following example demonstrates this:

//myfile.js

// Global variable
var iAmGlobalScope = 'foo';

function myFunctionA () {
    var iAmFunctionScope = 'bar';
    console.log(iAmGlobalScope); // Logs 'foo'
    console.log(iAmFunctionScope);// Logs 'bar'
}

function myFunctionB () {
    console.log(iAmGlobalScope); // Logs 'foo'
    console.log(iAmFunctionScope);// Throws ReferenceError: iAmFunctionScope is not defined
}

myFunctionA();
myFunctionB();

Variable declarations with var can trip up people who have come from other programming languages. Although they tend to understand global scope, they are not fully aware of the implications of function scope. The following example shows how people could be confused:

function odd (arg) {
    if (arg) {
        var a = true;
    } else {
        var b = true;
    }
    console.log(a, b);
}

odd(true); // Logs 'true, undefined'
odd(false); // Logs 'undefined, true'

People coming from other programming languages would probably think calling odd(true) would give a reference error when variable b is used at the end of the function, because the else block it’s defined in is never run, and the same with variable a when odd(false) is run. It is the function level scope in JavaScript which means that both variables are declared for the entire body of the function, but they have the value ‘undefined’ prior to a value being assigned to them. The best way to visualise this is if you imagine the JavaScript runtime rewrites the code to the following before executing it:

function odd (arg) {
    // Variable declarations hoisted to the start of the function.
    // Both will have the value 'undefined' at this point
    var a, b;
    if (arg) {
        a = true;
    } else {
        b = true;
    }
    console.log(a, b);
}

odd(true); // Logs 'true, undefined'
odd(false); // Logs 'undefined, true'

I think you’ll agree that the above code makes the result more obvious. The JavaScript engine does actually rewrite the code to be like this, and this is known as hoisting. The full details of hoisting are beyond the scope of this post, but suffice it to say function level scope is achieved by moving all var declarations within a function to the start of the function.

The Problem With var

Variable declarations being function rather than block level can cause a lot of problems. The best example of this is the classic closure in a loop interview question. See the code below.

// Assuming we have links with ids link-0 ... link-4
for (var i = 0; i <= 4; i++) {
    document.getElementById(`link-${i}`).addEventListener('click', function () {
        console.log(`${i} clicked`);
    });
}

Here we add click handler functions to each of our 5 links (0…4). We would expect clicking link 3 to log ‘3 clicked’ to the console, but instead it logs ‘4 clicked’. The same thing is logged for every link when clicked. You get ‘4 clicked’ every time, regardless of the link clicked. This happens because the variable i is scoped to whatever function this code is written in, not to the body of the for loop (the current block). The click handler functions use a reference to this variable i because the functions are closures. Since i is scoped to outside of the for loop the same i variable changes on each iteration of the loop. The final value which i is left with is 4, and so all the click handlers will get this value. Put simply each iteration of the loop shares the same i, whereas really the each need their own version of i.

let To The Rescue

It was decided that JavaScript needed block scoped variable declarations to solve issues such as this one, and so the ‘let’ keyword was introduced in ES6. ‘let’ is used to declare variables in the same way that var is, the only difference being that these variables will be scoped to the block in which they are defined, not the entire function. You can think of a block as anything that tends to wrap code in a set of curly braces (if, for, function, etc).

A very simple way to demonstrate the difference is to go back to our example of variables declared inside an if / else block. This time we will swap var for let:

function odd (arg) {
    if (arg) {
        let a = true;
    } else {
        let b = true;
    }
    console.log(a, b);
}

odd(true); // Reference error, a is not defined
odd(false); // Reference error, a is not defined

This time any call to the function will error. This is because variable a can only be accessed from within the if block within which it is defined, and b only from within the else block. At any other time these variables are said to be out of scope, which means it’s like they simply don’t exist. When we come to do the console.log call with the variables, they are out of scope because the console.log is not within the if or the else block. This results in an error as we are trying to use non existent variables.

So if we use let when declaring i in our problematic for loop:

// Assuming we have links with ids link-0 ... link-4
for (let i = 0; i <= 4; i++) {
    document.getElementById(`link-${i}`).addEventListener('click', function () {
        console.log(`${i} clicked`);
    });
}

Now we get the desired result. When we click link 3, we get ‘3 clicked’. Link for will be ‘4 clicked’ and so on. This is because using the block scoped let to declare i means that the variable is only in scope for the contents of the for block. Each closure created closes around that block, and so references the value as it currently is, without future increments of i affecting it.

Const For Immutability

ES6 also introduced yet another way to declare a variable (yes, that’s 3 ways!). The ‘const’ keyword is much like let, in that it is also block scoped. The difference is variables declared with const are constant. At this point the astute reader will be thinking I’ve made a mistake here. A constant variable is an oxymoron. Either something is constant, or it is a variable. We’ll go to yet another example to clear up the confusion.

function myFunc1 () {
    let bar = 1;
    bar = 2; // This is ok, we can reassign to variables declared with let
    const foo = 1;
    foo = 2;// This will break the function
}

myFunc1();// <span class="console-message-text">Uncaught <span class="object-value-error source-code">TypeError: Assignment to constant variable</span></span>

function myFunc2 () {
    let a;// This is ok
    const b;// Error!
}

myFunc2();// Error, missing initializer in const definition

function myFunc3 () {
    const bar = {};
    bar.foo = true;
    const list = [];
    list.push(1);
    console.log(bar, list);
}

myFunc3();// Logs: { foo: true }, [ 1 ]

myFunc1: This shows us that we cannot assign a new value to a const, like we can to a variable declared using let (or var).

myFunc2: This shows us we must assign a value to a const when declaring it.

myFunc3: This shows us that as long as we don’t reassign to a const, we can manipulate the value that the const references. Here we see that we can add properties to an object referenced by a const, and add items to an array referenced by a const.

The simple rule of const in JavaScript is this: You can only assign to a const once, but once assigned the value is mutable as long as no reassignment takes place. An even simpler way of putting it is that the rule with const is you can only use the ‘=’ operator once. As a side note doing something like i++ on a const is also an error, as the increment operator implicitly reassigns the incremented value to the variable.

This may seem strange to people coming from other programming languages. In other programming languages constants tend to be completely immutable. Take PHP for example:

define(‘SALES_TAX_RATE’, 20);

This gives us a SALES_TAX_RATE which will always be 20. You can only assign scalar (ie non object) values to constants in PHP, and they are usually used to give semantic meaning to some form of number or label which is used across the code.

Given this, people coming from other languages may question how useful const is in JavaScript. The main advantage is it makes the code a little easier to reason about, which makes it easier to maintain. For example look at the code below:

function foo () {
    let pi = 3.142;
    const b = 2;
    const obj = {};
    pi = 'apple';
    obj.bar = 1;
    pi = false;
}

A bit contrived, but we see here that we can keep changing the assignment of pi. At one point it’s a number, then later it becomes a string, and then finally a boolean. This means that pi means different things, and represents something different, at different points in the function. Contrast this to b and obj, which are consts. b will always be a number: 2, and obj will always be an object, even though we can mutate properties of that object. These consts will always be representing the same thing throughout the life of the code. They won’t be a ratio one moment and the name of a pie the next. When certain things are fixed, there’s much less for you to think about when developing. Less bugs will sneak into the code. You will be more productive.

Variable Declaration Best Practices

If you want to get a job at a tech firm, they will almost certainly have coding standards which define when to use var, const or let. These standards will virtually always be as follows:

  • var: Don’t ever use this.
  • let: Only use if you really must reassign to the variable latter. A good example of this is in say a for loop where you have a variable i that is incremented on each loop (i++). If you find you need to reassign to most variables that you use, you’re doing something wrong.
  • const: This should be used most of the time to declare variables, consider it the standard.

Leave a Reply

Your email address will not be published. Required fields are marked *