Home > Posts > Demystifying JavaScript Variable Declarations: let vs. var vs. const, Shadowing, and Scope

Demystifying JavaScript Variable Declarations: let vs. var vs. const, Shadowing, and Scope

Mohit Ranjan
Mohit Ranjan
Cover Image for Demystifying JavaScript Variable Declarations: let vs. var vs. const, Shadowing, and Scope
Mohit Ranjan
Mohit Ranjan

Let vs Var vs Const

Back in the day, there was only one type of variable declaration in JS, and that was var. It worked fine in the functional blocks, but for some reason, sometimes in big codebases, there were opportunities to re-declare a variable with the same name inside a non-function block (if statement). This caused issues because it also changed the declared variable.

Example :

var x = 5

if (true) {
  var x = 99
}

console.log(x) // 99

To overcome this issue, JS came up with new ways to declare variable that would solve this block scope problem. Yes, It was let, that solved it. Now when we declare a variable with let, its scope is inside the block, or say it is only valid inside the block it is declared in (Something like private variable).

In above example if we use let instead of var, the output would be 5, because declaration of x inside the block is for that block only. Its like that saying, 'what happens in vegas, stays in vegas 😉'. Now this invited a new error as well, Its like another saying, 'A small price to pay for salvation 🗿'. Enough with sayings now, so the issue is, we can not redeclare let in same scope as we could do with var. Which is quite understandable since let was introduced to solve the re declaration problem.

In ES6, they introduced one more way to declare a variable in javascript, const. It is just like let (block scoped), but more strict because we cannot change the value of a const declared variable by reassignment. Also, we need to initialize when declare it.

There is one other way to declare a JS variable as well. Other languages are pissed of javascript because of this way of declaration.

x = 99

Its more of an assignment rather than declaration, but yeah javascript will declare a variable called x in local scope, if you do not specify among let, var or const.

const b // Syntax error(missing declaration)
b = 77

const b = 77
b = 99 // Type error (assignment to const variable  )

We use const when we don't want it to be reassigned throughout the block. Although we could change the current value of a const variable without reassigning it.

Example :

const data = [1, 2, 3]
data[3] = 77
console.log(data) //[1, 2, 3, 77]

const newData = {name: 'Mohit'}
newData.name = 'FiftyCent'
console.log(newData) // {name: 'FiftyCent'}

Shadowing 🦇

Look at this example:

let x = 66

if(true){
  let x = 99
  console.log(x)
}

What do you think will be logged in console? The value wouldn't be re declaration error as expected (by some), but 99 instead. This happened because the variable x inside if block shadowed the one outside.

Now what do you think will be the output of this snippet?

let x = 66

if(true){
  var x = 99
  console.log(x)
}

There will be re declaration error. This happens because, x declared outside using let is in global scope, which means in global scope there can't be any more x to be declared again. When we declared x using var, it would be stored in global scope. Hence the error. If we used const inside the block, it would act same as let.

Now try and find the outputs from the below code:

(function () {
   try {
      throw new Error()
    } catch (x) {
      var x = 1,
      y = 2
      console.log(x)
    }
  console.log(x)
  console.log(y)
})()

I will explain it in the post credits

undefined vs Not defined

undefined means a variable has been initialized in the scope of js meanwhile not defined means it hasn't been initialized yet.

Example:

console.log(b) //undefined
console.log(x) //not defined
function run() {
  var x = 99
}
var b = 66

Here b was initialized in the first step of js code interpretation but x was not, because it was inside a function scope, So it went undetected and hence not defined.

The undefined state above is alternatively called Temporal dead zone of a variable.

Trust issues with setTimeout ft variables in JS

// Why does this function works well with let but not with var
function timer() {
  for (let i = 0; i <= 5; i++) {
    setTimeout(function() {
      console.log(i)
    }, i * 1000)
  }
}

let is a block scope variable while var and const are global scoped variables. Which means even when I declare a variable using var or const inside a block. It will be stored inside the global call stack, rather than the block. This won't happen in case of when you declare it using let.

This is the reason why it is advised to declare variables inside block using 'let'.

Now in above case what happened was, when the function timer got called, a loop started and finished without waiting for setTimeout to finish. So now there are six setTimeout executions are stored in call stack with a log of i.

When we use 'var': We Know, var is a global scoped variable and each time it was incremented, the same variable got updated as the incrementor of for loop ran. Now because of this after for loop was finished, in our global call stack we have a variable 'var i' whose value is 6 finally after 6 increments. So, When the setTimeout gets called six times, using the reference of i, it prints 6 six, times.

When we use 'let': Now let is different and is re declared in every block, so each time i got incremented, there was a new i getting stored for each setTimeout. So for first iteration i was 0, therefore setTimeout printed 0, for 2nd iteration, a new i (since let was re declared), has value 1, so setTimeout of second iteration prints 1 and the loop goes on in same way.

When we use 'const': Game is over because we cannot modify const ever, so in the very first attempt of modifying the global context 'const' by an incrementor, the code will show the error.

Solution to the previous problem

(function () {
  <!-- x and y hoisted here, step 2 and 3 -->
   try {
      <!-- Triggers an error to send the execution to catch -->
      throw new Error()
    } catch (x) {
      <!-- 1. x = Error at line something... -->
      var x = 1,
      <!-- 2. x is hoisted on the local scope -->
      <!-- 4. x is now assigned 1, but its not the x, that was hoisted, its the x which was parameter (due to shadowing).-->
      y = 2
      <!-- 5. y has value 2 assigned to it -->
      <!--3. y is also hoisted and sent to the top -->
      console.log(x)
      <!-- 6. Console log will have value 1 because of 4th statement -->
    }
  console.log(x)
  <!-- 7. Above console log will have value undefined because x was hoisted in the top of the block (being a var), and had value = undefined. -->

  console.log(y)
  <!-- 8. Lastly y will have value = 2 because of step 5-->
})()

P.S. Visit my github for more such stuff.