Hoisting / Scoping in JS

Many JavaScript programmers explain hoisting as JavaScript’s behavior of moving declarations (variable and function) to the top of their current scope (function or global). As if they were physically moved to the top of your code, which is not the case. For example:

console.log(a);
var a= 'Hello World!';

They will say, the above code will transform into this code after hoisting:

var a;
console.log(a);
a = 'Hello World!';

Although this is what appears to be happening (because the code is working fine), this isn’t actually happening, your code isn’t going anywhere. The JavaScript engine is not physically moving your code, your code stays where you typed it.

So What is Hoisting Then?

During compile phase, just microseconds before your code is executed, it is scanned for function and variable declarations. All these functions and variable declarations are added to the memory inside a JavaScript data structure called Lexical Environment. So that they can be used even before they are actually declared in the source code.

What is a Lexical Environment?

A lexical environment is a data structure that holds identifier-variable mapping. (here identifier refers to the name of variables/functions, and the variable is the reference to actual object [including function object] or primitive value).

This is what a lexical environment conceptually look like:

LexicalEnvironment = {
Identifier: <value>,
Identifier: <function object>
}

So in short, a lexical environment is a place where variables and functions live during the program execution.

Hoisting Function Declaration:

helloWorld();  // prints 'Hello World!' to the consolefunction helloWorld(){
console.log('Hello World!');
}

As we already know that function declarations are added to the memory during the compile stage, so we are able to access it in our code before the actual function declaration.

So the lexical environment for the above code will look something like this:

lexicalEnvironment = {
helloWorld: < func >
}

So when the JavaScript engine encounters a call to helloWorld(), it will look into the lexical environment, finds the function and will be able to execute it.

Hoisting Function Expressions

Only function declarations are hoisted in JavaScript, function expressions are not hoisted. For example: this code won’t work.

helloWorld();  // TypeError: helloWorld is not a functionvar helloWorld = function(){
console.log('Hello World!');
}

As JavaScript only hoist declarations, not initialisations (assignments), so the helloWorld will be treated as a variable, not as a function. Because helloWorld is a var variable, so the engine will assign is the undefined value during hoisting.

So this code will work.

var helloWorld = function(){
console.log('Hello World!'); # prints 'Hello World!'
}helloWorld();

Hoisting var variables:

Let’s look at some examples to understand hoisting in case of var variables.

console.log(a); // outputs 'undefined'
var a = 3;

We expected 3 but instead got undefined. Why?

Remember JavaScript only hoist declarations, not initializations. That is, during compile time, JavaScript only stores function and variable declarations in the memory, not their assignments (value).

But why undefined?

When JavaScript engine finds a var variable declaration during the compile phase, it will add that variable to the lexical environment and initialize it with undefined and later during the execution when it reaches the line where the actual assignment is done in the code, it will assign that value to the variable.

So the initial lexical environment for the above code will look something like this:

lexicalEnvironment = {
a: undefined
}

That’s why we got undefined instead of 3. And when the engine reaches the line (during execution) where the actual assignment is done, it will update the value of the variable in its lexical environment. So the lexical environment after the assignment will look like this:

lexicalEnvironment = {
a: 3
}

Hoisting let and const variables:

Let’s first take a look at some examples:

console.log(a);
let a = 3;

Output:

ReferenceError: a is not defined

So are let and const variables NOT hoisted?

The answer is a bit more complicated than that. All declarations (function, var, let, const and class) are hoisted in JavaScript, while the var declarations are initialized with undefined, but let and const declarations remain uninitialized.

They will only get initialized when their lexical binding (assignment) is evaluated during runtime by the JavaScript engine. This means you can’t access the variable before the engine evaluates its value at the place it was declared in the source code. This is what we call “Temporal Dead Zone”, A time span between variable creation and its initialization where they can’t be accessed.

If the JavaScript engine still can’t find the value of let or const variables at the line where they were declared, it will assign them the value of undefined or return an error (in case of const).

Let’s look at some more example:

let a;
console.log(a); // outputs undefined
a = 5;

Here during the compile phase, the JavaScript engine encounters the variable a and stores it in the lexical environment, but because it’s a let variable, the engine does not initialize it with any value. So during the compile phase, the lexical environment will look like this:

lexicalEnvironment = {
a: <uninitialized>
}

Now if we try to access the variable before it is declared, the JavaScript engine will try to fetch the value of the variable from the lexical environment, because the variable is uninitialized, it will throw a reference error.

During the execution, when the engine reaches the line where the variable was declared, it will try to evaluate its binding (value), because the variable has no value associated with it, it will assign it undefined.

So the lexical environment will look like this after execution of the first line:

lexicalEnvironment = {
a: undefined
}

And undefined will be logged to the console and after that 5 will be assigned to it and the lexical environment will be updated to contain the value of a to 5 from undefined.

Note — We can reference the let and const variables in the code (eg. function body ) even before they are declared, as long as that code is not executed before the variable declaration.

For example, This code is perfectly valid.

function foo () {
console.log(a);
}
let a = 20;
foo(); // This is perfectly valid

But this will generate a reference error.

function foo() {
console.log(a); // ReferenceError: a is not defined
}
foo(); // This is not valid
let a = 20;

Hoisting Class Declaration

Just as let and const declarations, classes in JavaScript are also hoisted, and just as let or const declarations, they remain uninitialized until evaluation. So they are also affected by the “Temporal Dead Zone”. For example:

let peter = new Person('Peter', 25); // ReferenceError: Person is  
// not defined
console.log(peter);
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
}

So to access the classes, you have to declare them first. For example:

class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
let peter = new Person('Peter', 25);
console.log(peter);
// Person { name: 'Peter', age: 25 }

So again during the compile phase, the lexical environment for the above code will look like this:

lexicalEnvironment = {
Person: <uninitialized>
}

And when the engine has evaluated the class statement, it will initialize the class with the value.

lexicalEnvironment = {
Person: <Person object>
}

Hoisting Class Expressions

Just as function expressions, class expressions are also NOT hoisted. For example, this code won’t work.

let peter = new Person('Peter', 25); // ReferenceError: Person is  
// not defined
console.log(peter);
let Person = class {
constructor(name, age) {
this.name = name;
this.age = age;
}
}

The correct way to do it is like this:

let Person = class {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
let peter = new Person('Peter', 25);
console.log(peter);
// Person { name: 'Peter', age: 25 }

Conclusion

  • What gets moved around is variable and function declarations. Variable assignments or initialization are NEVER moved around.
  • The declarations are NOT exactly moved to the top of your code; instead, they are put into memory.

In JavaScript, all variables defined with the var keyword have an initial value of undefined. This is due to hoisting which puts the variable declarations in memory and initializes them with the value of undefined. This behavior can be shown with the following example

line 2 throws a ReferenceError: y is not defined

However, variables defined with let or const keywords when hoisted are not initialized with a value of undefined. Rather, they are in a state called the Temporal Dead Zone and are not initialized until their definitions are evaluated.

line 1 throws ReferenceError because it is still in Temporal Dead Zone

The next code snippet demonstrates the hoisting of let and const variables.

line 3 throws a ReferenceError because x in line 4 has been hoisted within the block

The x variable defined with let keyword in the block is hoisted and takes precedence over the x variable defined with var. However, it is still in the temporal dead zone when it’s referenced in console.log(x) and so throws a reference error.

SCOPE

Variables defined with the var keyword have a scope that is its current execution context. They are not block scoped and so can be accessed from outside the block they were defined, provided it’s still within the scope of its execution context. Let and const variables, however, are block scoped and cannot be accessed from outside the block. This concept is shown below

some variables are referenced outside of their scope

Also, when you declare a global variable with var keyword, the variable gets attached to the global context (window in the browser and global in node). This is not the case with global variables declared with let and const.

GOTCHAS

  • When you don’t declare a variable but assign a value to the variable, the variable gets created and is attached to the global execution context (window in the browser and global in node). This, however, is strongly advised against as it makes debugging very difficult.
the variables get attached to the global context
  • Variables declared with var keyword can be redeclared at any point in the code even within the same execution context. This is not the case for variables defined with let and const keywords as they can only be declared once within their lexical scope.
cannot redeclare let and const variables.

This can especially cause a problem if you use let or const to declare a variable in multiple switch cases.

foo in both cases reside in the same block scope

Of course, it can be avoided by using curly braces around the cases to define different blocks, but this should probably be refactored.

foo in both cases reside in different block scopes
  • Another gotcha to remember about constants is that even though its value cannot be reassigned to, it is still mutable. The behavior can be seen in the fact that if the value is an object, the object’s properties can be modified.
variables defined with const are still mutable.

Experience with Front-end Technologies and MERN / MEAN Stack. Working on all Major UI Frameworks like React, Angular.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store