DEV Community

Nicolas Mesa
Nicolas Mesa

Posted on • Originally published at blog.nicolasmesa.co on

Var vs Let vs Const

Cross-posted from my personal blog https://blog.nicolasmesa.co.

Hi there!

In this post, we’re going to talk about javascript. We’ll start by looking at some examples of var, let and const variable declarations and their properties. Then we’re going to go through my recommendations of when to use each one.

var

var is the original way to do variable declaration in javascript. Here’s an example of how you can declare a variable using var:

var myVariable = 10;

var is function-scoped

Variables defined using var are function scoped. This means that the variable is available anywhere within its enclosing function. For example:

function functionScopedExample() {
    var a = 10;

    if (a < 20) {
        var b = 15;
    } else {
        var c = 30;
    }

    console.log(a, b, c); // logs: 10 15 undefined
}

There are a few things happening in this example. First, a is available everywhere in this function (like in most programming languages). Second, b is also available everywhere even though we declared and assigned it inside an if block. Third, c is also available everywhere in the function (even though our execution path doesn’t go into the else statement)! One thing to note here is that the value of c is undefined (since the code in the else statement is not executed), but c was still declared. To understand why this happened, we’ll need to learn about Variable Hoisting.

Here is another example of function-scoped variables:

function functionScopedExample2() {
    var a = 10;

    function innerFunction() {
        var b = 20;
        console.log(a); // logs: 10
    }

    innerFunction();

    console.log(a); // logs: 10
    console.log(b); // error: Uncaught ReferenceError: b is not defined
}

This example shows that a is available in its enclosing function (functionScopedExample2). This includes other functions defined within it (for example the innerFunction function). b, however, can only be referenced from its enclosing function (innerFunction) and not from outside.

Function-scoped declarations can have weird effects:

function functionScopedExample3() {
    var myNumberArray = [];

    for (var i = 0; i < 10; i++) {
        setTimeout(function() {
            myNumberArray.push(i);
        }, 100);

    }

    // wait until all timeouts have executed.
    setTimeout(function() {
        console.log(myNumberArray); // logs: [10, 10, 10, 10, 10, 10, 10, 10, 10, 10]
    }, 500);
}

The example above looks pretty weird. Most programmers would expect myNumberArray to have numbers from 0 - 9. Instead the array contains the number 10 10 times! This happens because the variable i is in scope for the whole function (functionScopedExample3). By the time the functions in the setTimeout execute, i already has a value of 10. One way to fix this issue is to create another function that receives i as an argument and executes the setTimeout:

function fixedFunctionScopedExample3() {
    var myNumberArray = [];

    // a new scope is created every time this function is called.
    // As a result, newI is isolated.
    function executeTimeout(newI) {
        setTimeout(function() {
            myNumberArray.push(newI);
        }, 100);
    }

    for (var i = 0; i < 10; i++) {
        executeTimeout(i);
    }

    // wait until all timeouts have executed.
    setTimeout(function() {
        console.log(myNumberArray); // logs: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
    }, 500);
}

Later, we’ll see how to solve this problem by using let instead of var.

Variable hoisting

When we define a variable using var, the variable declaration (not the assignment) is hoisted up to the beginning of the function. After the hoisting process, functionScopedExample (defined above) would end up looking like this:

function varHoistingExample() {
    // variables are hoisted to the top and all of them start
    // with the value of undefined.
    var a;
    var b;
    var c;

    // a gets its value here.
    a = 10;

    if (a < 20) {
        // b gets its value here.
        b = 15;
    } else {
        // c never gets its value so it remains undefined.
        c = 30;
    }

    console.log(a, b, c); // logs: 10 15 undefined
}

Hoisting can cause a lot of confusion. Take a look a the following example:

function weirdHoisting() {
    color = 'yellow';
    var color;

    console.log('My favorite color is', color); // logs: My favorite color is yellow
}

Wait, what? How did that work? Remember, first the variables defined with var are hoisted and then the function code is executed. In this example, the code after variable hoisting would look something like this:

function weirdHoisting() {
    // variable declaration is hoisted to the top of its enclosing function.
    var color;
    color = 'yellow';

    console.log('My favorite color is', color); // logs: My favorite color is yellow
}

Variable color’s declaration is hoisted and thus prevents any reference errors.

Named function declarations are hoisted as well (there are some edge-cases if you’re doing conditionals). Here’s an example:

function functionHoisting() {
    hoisted(); // logs: I am hoisted
    notHoisted(); // error: Uncaught TypeError: notHoisted is not a function

    var notHoisted = function() {
        console.log('This will not work');
    }

    function hoisted() {
        console.log('I am hoisted');
    }
}

In this example, we see that the hoisted function can be called before it is declared. The notHoisted variable, however, hasn’t been assigned the function by the time it is called. Note that the function doesn’t fail because the notHoisted name hasn’t been declared, but because it doesn’t point to a function (yet). After the hoisting process, this function would end up looking something like this:

function functionHoisting() {
    // variable and function declaration are hoisted.
    var notHoisted;

    function hoisted() {
        console.log('I am hoisted');
    }

    hoisted(); // logs: I am hoisted
    notHoisted(); // error: Uncaught TypeError: notHoisted is not a function

    // function would be assigned here but we never reach this.
    notHoisted = function() {
        console.log('This will not work');
    }
}

let and const

let is a newer way to define variables in javascript. Here’s an example of a variable definition using let:

let myVariable = 10;

const is a way to define constants in javascript. Here’s an example of a constant defined using const:

const myConstant = 200;

let and const are block-scoped

let and const are both block-scoped. This means that the variable/constant is available anywhere in the block after it has been declared. let/const definitions are NOT hoisted. Let’s go through a few examples:

function blockScopedExample() {
    let a = 10;

    if (a < 20) {
        let b = 15;
    } else {
        let c = 30;
    }

    console.log(a); // logs: 10
    console.log(b); // error: Uncaught ReferenceError: b is not defined

    // we would never reach this since the code fails in the line before
    // but this would be the output if it had been executed
    console.log(c); // error: Uncaught ReferenceError: c is not defined
}

This example shows that b and c are block-scoped. This is the reason why they’re not available outside of the if/else block. a was defined at the beginning of the function, making it available anywhere inside the function block. Declaring something with let at the beginning of a function is analogous to defining it with var.

Remember the example where our array ended up with the same values? Well, this problem is easily solvable by using let instead of var to define i:

function blockScopedExample2() {
    let myNumberArray = [];

    // let makes the variable i block-scoped
    for (let i = 0; i < 10; i++) {
        setTimeout(function() {
            myNumberArray.push(i);
        }, 100);

    }

    // wait until all timeouts have executed.
    setTimeout(function() {
        console.log(myNumberArray); // logs: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
    }, 500);
}

Since the variable i is block-scoped instead of function-scoped, its value remains the same for the whole block execution.

Here’s another example showing that variables defined with let are not hoisted:

function noHoisting() {
    color = 'yellow'; // error: Uncaught ReferenceError: color is not defined
    let color;

    // this is never reached.
    console.log('My favorite color is', color);
}

The output of this function is what most programmers coming from other languages expect (having to declare a variable before using it).

So far, every example (except for blockScopedExample2) we’ve covered using let would have the same outcome if we had used const instead. So, what does const give us?

Constants

const is how constants can be declared in javascript. After a constant has been defined using const, the constant can’t have another value assigned. Here’s an example:

function cantReassignConstants() {
    const myConstant = 10;
    myConstant = 9; // error: Uncaught TypeError: Assignment to constant variable
}

Constants defined with const also have to be assigned at declaration time (unlike let and var). For example, the following piece of code would fail during parsing (not at run-time).

function constantsCantBeDeclaredAndNotDefined() {
    const myConstant;
    myConstant = 5;
}

If you try to define this function, you would see an error like this:

Uncaught SyntaxError: Missing initializer in const declaration

Since this error occurs during parsing, the declaration of the function fails and we can’t even call it.

constantsCantBeDeclaredAndNotDefined(); // error: Uncaught ReferenceError: constantsCantBeDeclaredAndNotDefined is not defined

One thing to note about constants defined with const is that only the assignment is constant. The object assigned to a constant doesn’t become immutable. For example, the following code runs with no errors:

function constDoesntMakeObjectsImmutable() {
    const myObjectConstant = {
        myKey: 'myValue'
    };

    myObjectConstant.myKey = 'something else';

    console.log('myObjectConstant.myKey =', myObjectConstant.myKey); // logs: myObjectConstant.myKey = something else
}

Object.freeze()

If you want to make the object immutable, you can use Object.freeze() before assigning the object:

function freezingObject() {
    const myFrozenObject = Object.freeze({
        myKey: 'This value cannot be changed'
    });

    myFrozenObject.myKey = 'something else';

    console.log('myFrozenObject.myKey = ', myFrozenObject.myKey); // logs: myFrozenObject.myKey = This value cannot be changed
}

Even though the value of myKey didn’t change, the assignment didn’t fail either (it failed silently). If you want it to fail, use the 'use strict;' at the beginning of the function like this:

function freezingObject() {
    'use strict';
    const myFrozenObject = Object.freeze({
        myKey: 'This value cannot be changed'
    });

    myFrozenObject.myKey = 'something else'; // error: Uncaught TypeError: Cannot assign to read only property 'myKey' of object '#<Object>'

    // never executed
    console.log('myFrozenObject.myKey = ', myFrozenObject.myKey);
}

Object.freeze will only freeze the object that we pass as an argument. If the object contains references to other objects, those can still be modified unless we freeze them as well. For example:

function objectFreezeIsShallow() {
    const myFrozenObject = Object.freeze({
        key1: {
            reassignable: 'reassign me'
        },
        key2: Object.freeze({
            notReassignable: 'This value cannot change'
        })
    });

    myFrozenObject.key1.reassignable = 'reassigned';
    myFrozenObject.key2.notReassignable = 'this will not work';

    console.log('myFrozenObject.key1.reassignable =', myFrozenObject.key1.reassignable); // logs: myFrozenObject.key1.reassignable = reassigned
    console.log('myFrozenObject.key2.notReassignable =', myFrozenObject.key2.notReassignable); // logs: myFrozenObject.key2.notReassignable = This value cannot change
}

In this case, neither key1 or key2 can be reassigned since they’re frozen. The inner object of key1 however, is not frozen and the reassignable key is set to a different value. Note that the notReassignable key didn’t have its value reassigned since the object was frozen.

Recommendations

So, which one(s) should we use and why?

There are lots of reasons to use one over the other, so I divide this into a section per reason. Note that these are my preferences and it’s perfectly fine for other programmers to think differently :)

Never use the three of them in the same project

Decide which ones you want to use and stick to those. Using all three of them will be confusing. It will be difficult to know which one to pick and the code will be harder to understand. Be nice to the future readers of your code (including yourself).

Prefer consistency

If you’re working on an old project that is using var, keep using var. This will keep the project consistent.

Put variable declarations at the top if using var

If you’re using var, place all variable declarations at the top of the function. This will help you see which variables are available in the function and there won’t be any hoisting surprises.

Consider older browser support

If your code needs to support older browsers that don’t have let or const, consider using var instead. Alternatively, use a transpiler that converts all let and const declarations to var.

Prefer let over var

If you’re starting a new project, I would encourage you to use let over var. let is more predictable, especially for developers coming from a different language.

Prefer const over let when possible

I am a firm believer that everything should be a constant except for very special cases in loops or things like that (in those cases, fall-back to let). I’ll probably write another post on why using const (final in Java) is a good choice, but the gist is:

Debugging

It will ease debugging. For instance, consider the following code:

function useConstWhenPossible(myOtherObject) {
    const myObject = myOtherObject.getMyObject();

    // ...
    // 100 lines of code
    // ...

    myObject.doSomething(); // error: Uncaught TypeError: Cannot read property 'doSomething' of undefined

    // ...
    // 100 lines of code
    // ...

    return ...;
}

When you see the error, you go to the constant declaration. You can immediately tell that myOtherObject.getMyObject() returned undefined. There is no need to look through the other 100 lines of code to find if myObject ever gets reassigned.

Variable naming

You will have to come up with good names for your constants because you won’t be able to reuse them. This will make your code easier to read and more self-documenting.

Helps prevent bugs

Another developer (or maybe even you) won’t have a chance to define an existing variable and break the whole function.

Use named functions

This will save you a lot of headaches, especially when moving code around. You won’t need to make sure that that your function calls are after the function declarations and everything will work consistently.

Top comments (6)

Collapse
 
learosema profile image
Lea Rosema (she/her)

Great article!

One nice(or odd?) fact about older browser support: you can use let and const in IE11 without transpiling, even though it does not support most of the ES6+ features.

But I think, you may want to use a transpiler anyway for full ES2018 support.

Collapse
 
nicolasmesa profile image
Nicolas Mesa

I didn't know that! Thanks for the insight.

I agree that using a transpiler is still the way to go to ensure browser compatibility :)

Collapse
 
kipertech profile image
kipertech

Great article! Simple but yet provides clear understanding about these declararions which have been confusing me for a long time.

Thank you so much and looking forward to more like this in the future!

Collapse
 
nicolasmesa profile image
Nicolas Mesa

Thanks for reading! I'm glad you enjoyed it :)

Collapse
 
tangallan profile image
Allan Tang

Great article, this is a nice refresher !

Collapse
 
nicolasmesa profile image
Nicolas Mesa

Thanks for reading Allan! I'm glad you enjoyed it!