Why let, const and var can't work things out.

Listing out their differences

When I started writing my first programs in JavaScript, I used to assign all my bindings with var, I came from a Java background and I was astonished to see that here, we didn't need to define the type of the binding, just use a keyword and let the language do the work (and sometimes, create problems) but we're not here to talk trash about languages.

I started reading more into JavaScript, watched some tutorials online, and started using **let **instead of var, and I didn't know why, I just followed blindly because that's what lost people do, follow the crowd. Let's not be lost anymore.

History

JavaScript was invented by Brendan Eich in 1995, and from that time on, only **var **was available for everyone to define their bindings. 20 years into the implementation of the language, it was decided to introduce the let, and **const **keywords with the ES6 module to declare bindings, so other than their different natures, there's a huge generation gap between var and **let **& const.

Scope

Before we jump into the key differences between the various keywords for defining bindings, let's take a short detour to understand 'scope'.

In JavaScript and many other languages, the curly braces are used to define a new scope {}, it is completely 'legal' for a binding to be present inside several curly braces like

{{{{{let a= "Scope scope everywhere, no one to call"}}}}}

This kind of scoping is not something you'd come across easily, but it can exist. This brings us to our first difference. While, **let **and **const **and block-scoped, **var **is a little disrespectful when it comes to blocks, what I mean is that **var **is function scoped so if defined outside any function, they'll have global scope.

{
var statement=` "I don't care about boundaries" `;
}
console.log("var says", statement);
//output=> var says "I don't care about boundaries"

However, if the statement binding was created inside a function, we will not able to read it outside that function.

function providesFunctionScope(){
var statement="I respect bounderies of no one but functions"
console.log(statement); //this will read the value
}

console.log(statement); //no statement bindings available here, throws ReferenceError

Before we move on, what is block scope, which **let **and **const **bindings so dearly respect.

{
let letStatement=`"I'm not as rebellious as var, I stick with my braces."`;
console.log("let says,", letStatement); 
//output=> let says, "I'm not as rebellious as var, I stick with my braces"
}

console.log(letStatement); //Will throw ReferenceError.

Hoisting

JavaScript Hoisting refers to the process whereby the interpreter appears to move the declaration of functions, variables, or classes to the top of their scope, prior to execution of the code.

  • MDN Web Docs

The definition is good but what does it mean in layman's terms? Think of it as a flag being hoisted. Like a flag, the functions, variables, and classes are taken from their respective places and placed at the very top of their scope. So, I can write a function as the last line of code in the global scope and call it from anywhere above it and the program will not complain.

Many articles and tutorials will have you believe that **let **and **const **bindings are not hoisted but that is not the case. var, let, and const, all are hoisted but we can't access **let **and **const **bindings before they're declared and we get a ReferenceError which doesn't happen with var. Let's find out why with some examples.

For, **let **& **const **bindings

console.log(a);
let a=5;
//Ouput=> ReferenceError: a is not defined

For **var **bindings

console.log(a);
var a=10;
//output=> undefined

So, where does the difference lie? well, definitely not in hoisting. If you define a **let **or **const **binding and do not assign a value before reading it, we get undefined.

let a;
console.log(a);
//output=> undefined

To understand where the problem lies, we'd need to understand Temporal Dead Zone.

Temporal Dead Zone (TDZ)

The name sounds like it's from a science fiction movie referring to a radioactive zone, and thinking that wouldn't be too far from the truth. As living organisms die in radioactive zones, our code dies in the Temporal Dead Zone, but as the name suggests, it's only temporary.

var, let, and **const **all get hoisted but **var **gets initialized as well, while const and let don't, so any code that refers to a variable whose TDZ hasn't ended, will break the code and throw an error. Let's visualize this.

/*Temporal Dead Zone for variable alive
Temporal Dead Zone for variable alive
Temporal Dead Zone for variable alive
Temporal Dead Zone for variable alive
Temporal Dead Zone for variable alive
*/
let alive=true;  //Finally, Temporal Dead Zone Over
console.log("Is the code alive?", alive);
//output=> is the code alive? true

As you may have guessed already, **var **bindings don't have a Temporal Dead Zone.

Lexical Scoping

This is the topic most people get confused with, after understanding the scoping of variables and the Temporal Dead Zone, we tend to overlook a small but quite important detail in the JavaScript language, which is lexical scoping, which by definition means

A lexical scope in JavaScript means that a variable defined outside a function can be accessible inside another function defined after the variable declaration. But the opposite is not true; the variables defined inside a function will not be accessible outside that function.

This means, that a binding defined in the outer scope will be accessible inside inner scopes but not the other way around, but we already understood this, where does the confusion root from?

let scopedBinding=" I'm defined in the global scope";
{
let scopedBinding= "I'm defined in a block scope";
console.log(scopedBinding); //output=>I'm defined in a block scope
}

console.log(scopedBinding);
//output=> I'm defined in a the global scope

I hope this clears things up a bit but I can't help but wonder what would happen if I read the global binding in the block before I declare the block binding? Let's see.

let a=42;
{
console.log(a);
let a=5;
}

Any guesses for the output? 42? or 5? It will actually throw a ReferenceError, why? Since there is a binding defined with the same name in the local scope of the block, it will access that and end up in what we read about earlier i.e. The Temporal Dead Zone.

If we were to only reassign the value of the 'a' binding, our console will log 42 and then change the value of 'a' to 5.

Redeclaration

They have but one more difference between them, while **var **bindings can be redeclared, **let **and **const **bindings can't, i.e if a **var **binding already exists, we can redeclare that same binding to reference something else, not to be confused with re-assigning, for example,

var batman="I'm Batman";
var batman =false;  //I have used var again here
console.log("are you Batman?", batman);
//output=> are you Batman? false;

Notice, the second line, I'm redeclaring the binding, not reassigning. To re-assign, we only need to mention the binding name and change it howsoever.

The above code runs fine, but if we were to do the same with a **let **and **const **binding, we'd get an error.

let spiderman="Hii, I'm Peter Parker";
let spiderman=false;
console.log("Are you spiderman", spiderman);
//output=> SyntaxError: Identifier 'spiderman' has already been declared

This holds true for both **let **and **const **bindings as stated earlier.

Reassigning

Till now, we've been using **let **and **const **interchangeably and noting the difference between them and **var **declarations, however, it's now time to see how **let **differs from const. **var **and **let **variables have no issues being reassigned

let greeting="Hello!";
greeting="Merry Christmas";
console.log(greeting);
//output=> Merry Christmas

However, if we try to do the same with a **const **binding, we get a TypeError, for example,

const greeting="Hello!";
greeting="Merry Christmas";
//output=> TypeError: Assignment to constant variable.

However, we can change a **const **binding object's property without angering anyone. For a **const **array=>

const numberArray=[1,2,3,4,5];
numberArray.push(6);
console.log(numberArray);
//output=> [1,2,3,4,5,6];

For a **const **object=>

const author={age:22, name:"User"};
person.name="Prabhav";
console.log(person);
//output=> {age:22, name: 'Prabhav'}

So, a good rule of thumb is that **const **bindings can't be re-assigned, binding object's properties are.

TL:DR

**var **was introduced way before **let **& const. The **let **and **const **variables were part of the ES6 released in 2015. While **var **has function scope, **let **& **const **are block-scoped. All the three types of variables are hoisted but only **var **gets initialized at the same time and **let **& **const **bindings enter what is called a Temporal Dead Zone(TDZ). **var **allows both redeclaration and reassignment, however, **let **only allows reassignment, and **const **prohibits both.

If I've missed something, please add it in the comments below.