DEV Community

Cover image for Hoisting of Variables, Functions, Classes, Types, Interfaces in JavaScript/TypeScript
Anton Zamay
Anton Zamay

Posted on • Edited on

Hoisting of Variables, Functions, Classes, Types, Interfaces in JavaScript/TypeScript

In the world of JavaScript, understanding the concept of hoisting can significantly improve the way you write and debug your code. Hoisting is one of those interesting behaviors of JavaScript that can sometimes lead to unexpected results, especially for those new to the language. In this article, we'll explore what hoisting is, how it works, and look at some examples to get a clearer picture.

TOC

Variable Hoisting

Variable hoisting is a JavaScript mechanism where variable declarations are moved to the top of their containing scope before the code has been executed. This means that no matter where variables are declared, they are moved to the top of their scope by the JavaScript interpreter.

Importantly, only the declarations are hoisted, not the initializations. This distinction is crucial for understanding some of the puzzling behavior that can arise.

To illustrate how variable hoisting works, let's look at a few examples.

Example 1: Basic Variable Hoisting

console.log(myVar); // Output: undefined
var myVar = 5;
Enter fullscreen mode Exit fullscreen mode

In this example, you might expect a ReferenceError since myVar is printed before it's declared. However, due to hoisting, the JavaScript interpreter sees the code as follows:

var myVar;
console.log(myVar); // Output: undefined
myVar = 5;
Enter fullscreen mode Exit fullscreen mode

Example 2: let and const Hoisting

The introduction of let and const in ES6 has somewhat altered the landscape of hoisting. Unlike var, which is hoisted to the top of its scope, let and const are hoisted to the top of their block scope but are not initialized, leading to what is called a "Temporal Dead Zone" (TDZ).

console.log(myLetVar); // ReferenceError: Cannot access 'myLetVar' before initialization
let myLetVar = 3;
Enter fullscreen mode Exit fullscreen mode

In this scenario, myLetVar is hoisted, but trying to access it before declaration results in a ReferenceError.

Example 3: Re-declaring Variables with var

var myVar = 1;
console.log(myVar); // Output: 1

var myVar = 2;
console.log(myVar); // Output: 2
Enter fullscreen mode Exit fullscreen mode

In this example, despite myVar being declared twice, JavaScript's hoisting mechanism processes and consolidates the declarations at the top of their scope. The reassignment of myVar to 2 happens after the initial assignment, leading to the final value of 2 being logged. Below is how the JavaScript engine interprets the code as if it's written like this:

var myVar;
myVar = 1;
console.log(myVar);

myVar = 2;
console.log(myVar);
Enter fullscreen mode Exit fullscreen mode

Function Hoisting

In JavaScript, functions are subject to hoisting much like variables are. However, how function hoisting works depends on how the function is declared.

Example 4: Function Declaration Hoisting

Function declarations are fully hoisted. This means that the entire body of the function, along with its declaration, is hoisted to the top of its scope. Here's how it works:

console.log(myFunc()); // Output: "Some text!"

function myFunc() {
  return "Some text!";
}
Enter fullscreen mode Exit fullscreen mode

Example 5: Function Expression Hoisting

Function expressions behave differently. If a function is assigned to a variable, the variable declaration is hoisted but not the assignment. This distinction is crucial for understanding how different function declaration methods behave regarding hoisting.

Consider this example using a function expression with var:

console.log(myFunc); // Output: undefined
console.log(myFunc()); // TypeError: myFunc is not a function

var myFunc = function() {
  return "Some text!";
};
Enter fullscreen mode Exit fullscreen mode

Here, the variable myFunc is hoisted to the top of its scope, meaning it exists and is undefined when we try to log it or call it. This results in a TypeError when attempting to invoke myFunc as a function before the function expression is assigned to it.

For function expressions using let or const, an attempt to use them before declaration will lead to a ReferenceError because let and const declarations are in a TDZ as described earlier.

Class Hoisting

Classes in JavaScript exhibit the same hoisting behavior to variables declared with let and const. When you declare a class, its declaration is hoisted to the top of its enclosing scope, much like let and const. However, just as let and const declarations are not initialized until the JavaScript engine executes their declaration line, classes too are not initialized until the engine evaluates their declaration. This also results in a TDZ, meaning any attempts to access the class before its declaration will result in a ReferenceError. So, dealing with inheritance, static methods, and other object-oriented programming constructs, ensure that base classes are defined before derived classes try to extend them, respecting the TDZ.

Type Alias & Interface "Hoisting" in TypeScript

TypeScript's type aliases and interfaces are purely a design-time construct, meaning they are used by the TypeScript compiler for type checking and are erased when the TypeScript code is compiled to JavaScript. So, the concept of hoisting doesn’t really apply to type aliases or interfaces because they do not exist in the compiled output and, therefore, have no impact on the runtime behavior of the code.

For example:

let myVar: MyType = { key: "value" };

type Animal = {
    key: string;
};
Enter fullscreen mode Exit fullscreen mode

In the code above, even though the MyType type is used before its declaration, TypeScript does not throw an error. This is because, from the perspective of the TypeScript type checking system, all type declarations are considered to be known ahead of any value-space code execution or evaluation. This isn't hoisting in the traditional JavaScript sense, but rather a feature of TypeScript's static analysis that all type declarations are globally known to the compiler before any code execution.

Tricky Tasks

Task 1.

var myVar = 1;

var myFunc = function() {
    console.log(myVar); // Output: ?

    var myVar = 2; 

    console.log(myVar); // Output: ?
}

myFunc();
Enter fullscreen mode Exit fullscreen mode

Task 2.

function myFunc() {
    return 1;
}
console.log(myFunc()); // Output: ?

function myFunc() {
    return 2;
}
console.log(myFunc()); // Output: ?
Enter fullscreen mode Exit fullscreen mode

Task 3.

(function() {
    try {
        throw new Error();
    } catch (x) {
        var x = 1, y = 2;
        console.log(x); // Output: ?
    }
    console.log(x); // Output: ?
    console.log(y); // Output: ?
})();
Enter fullscreen mode Exit fullscreen mode

Solutions

Task 1.

  • When myFunc is invoked, JavaScript hoists the declaration of myVar to the top of the function scope. This means the code inside myFunc behaves as if it was written like this:
var myVar = undefined;

console.log(myVar); // Output: undefined

myVar = 2;

console.log(myVar); // Output: 2
Enter fullscreen mode Exit fullscreen mode
  • Therefore, the first console.log(myVar) outputs undefined because the declaration (but not the initialization) of myVar is hoisted to the top of the function scope, overshadowing the myVar declared outside the function scope.
  • After myVar is assigned the value of 2 within the function, the second console.log(myVar) outputs 2.

Task 2.

  • Both function declarations are hoisted to the top of their scope. However, the second declaration of myFunc overwrites the first. This is because in JavaScript, function declarations are hoisted above variable declarations and are processed before the code begins to run.
  • Therefore, regardless of where the functions are declared, the version of myFunc that finally resides in memory after hoisting and interpretation is the one that returns 2.
  • Both console.log(myFunc()); calls will output 2, because that's the version of myFunc being used after hoisting and overwriting.

Task 3.

  • In the catch block, variables myVar1 and myVar2 are declared. Since var declarations are function-scoped (not block-scoped like let or const), both myVar1 and myVar2 are hoisted to the top of their enclosing function.
  • Therefore, console.log(myVar1); within the catch block outputs 1.
  • Outside the catch block but inside the self-invoking function, both myVar1 and myVar2 are accessible due to hoisting, and since they were assigned values in the catch block, their values are preserved outside of the catch block.
  • console.log(myVar1); outside the catch block also outputs 1.
  • Similarly, console.log(myVar2); outputs 2.

Top comments (0)