by Chris Lyons

Learn JavaScript Fundamentals by Understanding the Differences Between Var, Let, and Const

For many years, the only way to create a variable in JavaScript was the var keyword. It has always frustrated programmers who were accustomed to the way variables work in other languages. Finally, with the arrival of ES6, we were given let and const to solve some of the issues we’ve had with var. There are several key differences between var, let and const. Let’s explore these differences and learn more about JavaScript fundamentals along the way.

First, though, let’s cover some terminology for variables: keyword, declaration, and initialization.

terminology.js
Copy

const person1 = "chris"; // const is the `keyword`
const person2 = "chris"; // person2 is the `declaration`
const person3 = "chris"; // anything after the "=" is the `initialization`

Let’s also clear up assignment by value vs assignment by reference. JavaScript has seven primitive types: Null, Undefined, Boolean, Number, BigInt, String and Symbol. It also has three reference types: Object, Array and Function. When a primitive type is assigned to a variable, it is assigned by value since the variable is stored in the memory stack as the actual value. Alternatively, when assigning a non-primitive type to a variable, it is assigned by reference, meaning the value that is actually stored is a reference to the object in the heap memory. This is an important detail to keep in mind as we get further into this article.

1. Declaration and Initialization

Let’s start with some of the basic differences between the variable keywords. A let or var can be declared without being initialized (assigned a value or reference), while a const must be initialized when it is declared. Let’s look at an example:

declaration.js
Copy

var person1; // this works
let person2; // this also works
console.log(person1, person2); // outputs: undefined undefined
const person3; // this does NOT work
// Uncaught SyntaxError: Missing initializer in const declaration

2. Re-declaration

A var can be re-declared, while a let or const cannot be.

reDeclaration.js
Copy

var person1 = "chris";
let person2 = "will";
const person3 = "elliott";
var person1 = "new chris"; // this works
let person2 = "new will"; // this does NOT work
// Uncaught SyntaxError: Identifier 'person2' has already been declared
const person3 = "new elliott"; // also does NOT work
// Uncaught SyntaxError: Identifier 'person3' has already been declared

3. Reassignment

A var or let can be reassigned a value or reference, while const cannot be reassigned, as it has limited mutability.

reassignment.js
Copy

var person1 = "chris";
let person2 = "will";
const person3 = "elliott"
person1 = "new chris"; // this works
person2 = "new will"; // this also works
console.log(person1, person2); // outputs: new chris new will
person3 = "new elliott"; // this does NOT work
// Uncaught TypeError: Assignment to constant variable.

However, if your const is an Object or Array, you can add or remove items, and also mutate property values. You just can’t reassign the reference to the object itself.

reassignment.js
Copy

const person = {
name: "John Smith",
age: 36
}
person.jobTitle = "Senior Front End Engineer"; // this works
person.age = 37; // this also works
person = {}; // this will NOT work
person = "string"; // this will NOT work
// Uncaught TypeError: Assignment to constant variable.

4. Scope

The next difference we will discuss is scope. var has function scope. This means that a variable declared with the var keyword is available anywhere inside the function in which it was declared. let and const have block scope, meaning they are only available inside the block in which they are declared. You can think of a block as anything surrounded by curly braces {}, like loops and if statements. Anything not declared in a block (or function in the case of var) will have global scope.

JavaScript uses lexical scoping, meaning inner blocks or functions have access to the variables defined in the outer blocks or functions, but this generally does not work the other way around.

In the example below, the inner function is making use of variables declared in the outer scope. This is also known as a closure.

lexicalScope.js
Copy

// these are all in the GLOBAL scope
var myVar = "myVar";
let myLet = "myLet";
const myConst = "myConst";
// inner function has access to these variables
const inner = () => {
console.log(myVar, myLet, myConst);
}
inner() // outputs: myVar myLet myConst

In the following example, all of the variables are scoped to the inner function, so they are not available in the global scope. let and const are block scoped, and the function is considered a block. var has function scope, so it is also scoped to the function.

functionScope.js
Copy

const inner = () => {
var myVar = "myVar";
let myLet = "myLet";
const myConst = "myConst";
}
inner();
console.log(myVar); // Uncaught ReferenceError: myVar is not defined
console.log(myLet); // Uncaught ReferenceError: myLet is not defined
console.log(myConst); // Uncaught ReferenceError: myConst is not defined

In the example below, the let and const are scoped to the if statement because they have block scope, so they are not available in the global scope. However, the var is available in the global scope because it has function scope rather than block scope.

blockScope.js
Copy

if (true) {
var myVar = "myVar";
let myLet = "myLet";
const myConst = "myConst";
}
console.log(myVar); // output: myVar
console.log(myLet); // Uncaught ReferenceError: myLet is not defined
console.log(myConst); // Uncaught ReferenceError: myConst is not defined

Another strange behavior of var is that you can create a variable without declaring it by simply assigning it a value. If it was undeclared, it will be created with the var keyword on the global scope.

undeclared.js
Copy

person = "chris";
console.log(person); // output: chris
// you can verify it is created with the `var` keyword using the below try/catch
try {
eval('var person');
// it was undeclared, or declared using `var`
console.log("'person' was created with the `var` keyword");
} catch (error) {
// it was declared with `let` or `const`
console.log("'person' was created with the `let` or `const` keyword")
}

This strange behavior leads us to the final difference we will be discussing today, hoisting.

5. Hoisting

Hoisting is JavaScript’s default behavior of moving all declarations to the top of the current scope. We are all relatively accustomed to this behavior as it relates to function declarations, as in the example below:

hoistedFunction.js
Copy

doSomething(); // output: it still works
function doSomething() {
console.log("it still works");
}

Variables defined with the var keyword and function declarations are hoisted to the top of the scope. Keep in mind, though, that JavaScript only hoists declarations, not initializations.

The following code demonstrates this concept with the var keyword:

hoistedVar.js
Copy

person = "chris"; // this is initialized before being used
console.log(person); // output: chris
var person; // the declaration is hoisted
console.log(person2); // output: undefined
person2 = "chris"; // this is initialized after being used
var person2; // this declaration is hoisted and set to "undefined"
console.log(person3); // Uncaught ReferenceError: person3 is not defined
// person3 is never declared, so no hoisted declaration exists

Variables defined with let and const are not hoisted, as illustrated by the example below:

notHoisted.js
Copy

console.log(letPerson); // Uncaught ReferenceError: letPerson is not defined
let letPerson = "let-person";
console.log(constPerson); // Uncaught ReferenceError: constPerson is not defined
const constPerson = "const-person";

And, finally, this last example demonstrates the difference in hoisting for a function declaration and a function expression:

hoistedDeclaration.js
Copy

// The following WORKS because it uses a `function declaration`,
// and declarations ARE hoisted.
sayHello(); // output: hello
function sayHello() {
console.log("hello");
}
// The following DOESN'T WORK because it is a `function expression`, meaning
// it's assigned to a variable - and assignments AREN'T hoisted.
sayHello(); // ReferenceError: Cannot access 'sayHello' before initialization
const sayHello = () => {
console.log("hello 2")
}

So What Keyword Should You Use, and in Which Situations?

The bottom line is that you should stop using var. If you find yourself needing to use it, I’d highly recommend re-thinking the design of your code. I would recommend defaulting to the const keyword unless you need a primitive value you can mutate, then use let in those cases. And lastly, always define your variables at the top of your scope. These simple tips will improve the reliability of your JavaScript code.

Thanks for reading, and happy coding!