Understanding the variable environment and hoisting in JavaScript

In this article, we will learn about hoisting and how it works under the hood. We'll explore the hoisting behavior of variables created with var, let, and const, as well as function and class declarations and expressions, and arrow functions.

Let's start with the concept of hoisting and how it works behind the scenes.

Hoisting

Hoisting refers to the process of elevating certain variables to the top of their respective scope. Because of hoisting, we can access some variables before they are created within a scope. That's what hoisting appears to be on the surface.

Under the hood, the JavaScript engine scans through the code and creates a new property for each variable in a place called the variable environment. This happens before the code execution starts and is a part of the execution context creation.

Hoisting behaves differently for different types of variables, functions, and classes. Let's examine the distinctions.

Variables created with ‘var’

Variables created with var are hoisted and placed in the variable environment with an initial value set to undefined.

console.log(name); // undefined

var name = 'John';

As you can see in the above code, attempting to access the name variable before it's created does not result in an error; instead, we get undefined.

Variables created with ‘let’ and ‘const’

Technically, variables created with let and const are also hoisted, but they are placed in the temporal dead zone with their initial value set to uninitialized.

Unlike variables created with var, trying to access a variable created with let or const before it's created results in a reference error, as you can see in the following code.

console.log(name); // ReferenceError: Cannot access 'name' before initialization

let name = 'John';

For this behavior, variables created with let and const may seem like they are not hoisted, but actually, they are.

Function declarations

Function declarations are hoisted and placed in the variable environment with an initial value set to the actual function. As a result, we can call a function declaration before it's created.

greet();

function greet() {
  console.log('Good morning pal!'); // Good morning pal!
}

As you can see in the above example, we can call the greet function before it's created in the code due to hoisting.

Function expressions and arrow functions

Function expressions and arrow functions created with var are not hoisted. So, trying to call such a function before it's created results in a type error.

// Function expression
logger(); // TypeError: logger is not a function
var logger = function () {};

// Arrow function
greet(); // TypeError: greet is not a function
var greet = () => {};

However, function expressions and arrow functions created with let and const follow the same hoisting principles as variables created with these keywords.

In other words, function expressions and arrow functions created with let and const are technically hoisted but placed in the temporal dead zone with the initial value set to uninitialized.

// Function expression
logger(); // ReferenceError: Cannot access 'logger' before initialization
let logger = function () {};

// Arrow function
greet(); // ReferenceError: Cannot access 'greet' before initialization
const greet = () => {};

As a result, attempting to access such a function before it's created results in a reference error, as you can see in the above examples.

Class declarations and expressions

Class declarations are technically hoisted, but they are placed in the temporal dead zone with their initial value set to uninitialized. So, trying to use a class declaration before it's created results in a reference error, as you can see in the following code.

const john = new Person();
john.height = 100;
john.weight = 300;

console.log(john); // ReferenceError: Cannot access 'Person' before initialization

class Person {
  constructor(height, weight) {
    this.height = height;
    this.weight = weight;
  }
}

Similarly, class expressions created with let and const are also technically hoisted, but they are placed in the temporal dead zone with their initial value set to uninitialized. So, trying to use such a class expression before it's created results in a reference error. The following is an example.

const john = new Person();
john.height = 100;
john.weight = 300;

console.log(john); // ReferenceError: Cannot access 'Person' before initialization

const Person = class {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
};

However, class expressions created with var are not hoisted. Thus, attempting to use such a class expression before it's created results in a type error, as you can see in the following code.

const john = new Person();
john.height = 100;
john.weight = 300;

console.log(john); // TypeError: Person is not a constructor

var Person = class {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
};

Temporal dead zone

A temporal dead zone for a variable is the portion of a scope that precedes the creation of the variable. Trying to access a variable within the temporal dead zone results in a reference error. The following is an example.

console.log(name); // ReferenceError: Cannot access 'name' before initialization

let name = 'John';

It's worth noting that trying to access a variable that is never created results in a slightly different reference error, as you can see in the code below.

console.log(name); // ReferenceError: name is not defined

We get these slightly different errors because the JavaScript engine knows the variables that don't exist at all. It also knows that variables in the temporal dead zone exist and will eventually become available.

Having the temporal dead zone, we can write better JavaScript code and minimize bugs in our applications. It also ensures that const behaves as intended, preventing reassignment.

There you have it! I hope this explanation has given you a clearer understanding of how hoisting works under the hood in JavaScript and how different variables, functions, and classes are hoisted.

Hoisting is one of three topics of a JavaScript execution context. If you are interested in learning the other two topics, I recommend checking out the articles on Understanding the dynamic nature of the ‘this’ keyword in JavaScript, as well as Exploring the scope and scope chain in JavaScript for further insights.

Share this article with your friends

Copy URL

Elevate your JavaScript and freelance journey

Supercharge your JavaScript skills and freelance career. Subscribe now for expert tips and insights!