Scope Chain in JavaScript

const and let are the new block-scoped variable declaration methods introduced in ES6. const declares a constant variable, so what are the differences between them and var?

var a = 0;
if (true) {
  console.log(a)
  let a = 0;
  console.log(a)
}

This code will throw a ReferenceError. Variable a is declared with let in a block-level scope. If we use variable a before its declaration, we will enter the Temporal Dead Zone (TDZ). Because the variable a has been found in the block-level scope, it will not search for any other variable a in the scope chain. Referencing it within the TDZ will throw a ReferenceError.

Scope Chain

When a variable is referenced in the code, the JavaScript engine starts searching for that variable from the current scope and keeps going up the scope chain until it finds the variable or reaches the global scope. This search process is called the Scope Chain. If the variable is found, the value in the corresponding scope will be used; otherwise, a ReferenceError will be thrown.

Variable look up in scope chain

Variable Hoisting🏗 and the Temporal Dead Zone of let and const

During the compilation process before executing the code, the JavaScript engine will scan the entire scope and “hoist”🏗 all the function and variable declarations to the top of the scope.

For var, let, and const, both the creation and initialization are hoisted, and initialize the value to undefined, but assignments are not hoisted.

Variables or constants declared using let or const have a “Temporal Dead Zone” (TDZ) in which they are not yet initialized and cannot be accessed from the beginning of the current block scope until the variable or constant is declared. Attempting to access the variable or constant within this zone will result in a ReferenceError being thrown.

For a function, its creation, initialization, and assignment are all hoisted at the same time.

When the execution context is created at compilation time, f1 is stored, and v1 and v2 are both initialized to undefined. However, v2 is created using let and is referenced within the TDZ, causing a reference error to be thrown.

Block Scope

In ES5, there are only two types of scopes, global scope and function scope. However, ES6 introduces block scope, which is only valid within the block in which it is declared using the let or const keyword.

Block scope can prevent variable pollution and naming conflicts. Variables declared within a block are released after the block is executed, which means they won’t pollute the global scope.

var a = 10
let a = 11 // caught SyntaxError: Identifier 'a' has already been declared
{
  let a = 12
  console.log(a) // 12
}
{
  const a = 13
  console.log(a) // 13
}

Block-level scope is delimited by braces {}, as well as by constructs such as while(){}, if(){}, try{} catch(e){}, and for loops.

In a for loop, each iteration creates a separate and independent scope.

var a = []
for (let i = 0; i < 10; i++) {
  a[i] = function () { console.log(i); };
}
a[6](); // output: 6 

// If we change let to var
var a = []
for (var i = 0; i < 10; i++) {
  a[i] = function () { console.log(i); };
}
a[6](); // output: 10 
// we can use the value as a parameter to separate the scope
var a = []
for (var i = 0; i < 10; i++) {
  (function(i) {
    a[i] = function () { console.log(i); }
  })(i)
}
a[6](); // output: 6 

Variables and constants declared with let and const are limited to the current block scope and are not accessible outside of this block scope.

While block scope can limit the scope of variables, it does not allocate a new execution context for variables declared within a block. In JavaScript, only functions create new function execution contexts, not block scopes.

The block scope is still within the same execution context of the foo() function, and the code inside the block scope will be released when it is executed completely.