DEV Community

Cover image for 🚀How JavaScript Works (Part 4)? Hoisting, var, let, const, TDZ
Sam Abaasi
Sam Abaasi

Posted on

🚀How JavaScript Works (Part 4)? Hoisting, var, let, const, TDZ

JavaScript, often perceived as an interpreted language due to its runtime execution, is, in fact, a language with a two-step process, including a critical compilation stage. The compilation process comprises tokenization/lexing, parsing, and actual code compilation, making JavaScript more than just a simple scripting language. This compilation stage is essential to understand the concept of hoisting.

Table of Contents

The Compilation Stage

Before a JavaScript program executes, it goes through a series of phases during the compilation stage:

  • Tokenization/Lexing:
    The engine breaks the source code into chunks called tokens. Tokens are the smallest units of a program, like words in natural language.

  • Parsing: The engine analyzes the syntax of the token stream to create an abstract syntax tree (AST). The AST represents the grammatical structure of the code and helps the engine understand its meaning.

  • Compilation: During this phase, the engine translates the AST into executable code, often in the form of bytecode or machine code. This compiled code is optimized for execution.

The Role of Parsing, AST, and Scope Analysis for Hoisting

When a JavaScript program is executed, the first crucial step is the parsing of the source code. Parsing is the process of taking raw JavaScript code and transforming it into an Abstract Syntax Tree (AST). This tree-like structure represents the grammatical and structural elements of the code.

The AST, in combination with scope analysis, plays a pivotal role in the process of hoisting.

Scope analysis involves identifying where in the code certain variables and functions are valid or accessible. It is during this analysis that JavaScript engines determine the boundaries of various scopes in your code.

How Does Hoisting Fit into This Process?

Hoisting is not some mystical mechanism but rather a direct consequence of this parsing and scope analysis. It is essentially the act of re-arranging variable and function declarations and attaching them to the top of their appropriate scopes.

Let's break it down:

  • Variable Declarations:
    When you declare a variable using var, for instance, the JavaScript engine identifies it during the parsing phase. It then hoists this declaration to the top of the current scope, ensuring that the variable can be accessed anywhere within that scope. This means that you can use a variable before it's declared in your code without causing an error.

  • Function Declarations: Similarly, function declarations are also hoisted. The engine recognizes them during parsing, moves them to the top of the current scope, and assigns a reference to the function. This allows you to call a function before it's formally defined in your code.

By performing this hoisting process based on the information gathered from parsing and scope analysis, JavaScript ensures that variables and functions are accessible where they should be. This is why hoisting is often described as just a matter of re-arranging these declarations to the top of their appropriate scopes.

Hoisting with Variable Declarations

Let's start with variable declarations. Consider this code:

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

The magic happens here! Despite trying to console.log the variable a before it's declared, JavaScript doesn't throw an error. Instead, it hoists the variable declaration to the top of the current scope, making it available but uninitialized. The code is effectively treated as if written like this:

var a; // Declaration is hoisted
console.log(a); // Outputs: undefined
a = 5; // Initialization still happens where you originally defined it
console.log(a); // Outputs: 5
Enter fullscreen mode Exit fullscreen mode

Hoisting with Function Declarations

Now, let's see how function declarations are hoisted:

sayHello(); // Outputs: "Hello, World!"
function sayHello() {
  console.log("Hello, World!");
}
Enter fullscreen mode Exit fullscreen mode

In this case, we're calling the sayHello function before its actual declaration. JavaScript hoists the function declaration to the top of the current scope, making it accessible before its appearance in the code. The code behaves as if it were written like this:

function sayHello() {
  console.log("Hello, World!");
}
sayHello(); // Outputs: "Hello, World!"
Enter fullscreen mode Exit fullscreen mode

This demonstrates how function declarations are hoisted in JavaScript, allowing them to be used before they are formally defined in the code.

Variable Declarations and Function Declarations

To grasp the nuances of hoisting, it's essential to differentiate between variable declarations and function declarations. The behavior differs significantly between these two constructs.

Function Declarations

Function declarations are hoisted entirely, encompassing both the declaration and initialization phases. This means that when a function is declared using the function keyword, it is effectively recognized as a function declaration. As a unique feature, function declarations don't require an assignment operator (=) since the JavaScript engine comprehends that they are meant to create a function. This hoisting behavior of function declarations aligns with the rules set forth by the ECMAScript language standard.

The rationale behind hoisting function declarations in their entirety is grounded in the behavior specified in the ECMAScript standard. In JavaScript, functions are considered "first-class citizens," signifying that they are treated as objects and can be referenced much like any other variable or value.

The Benefits of Function Hoisting:

This full hoisting enables the calling of a function before its formal declaration within the code, fostering flexibility in code organization. It also contributes to more readable code, particularly in situations where functions invoke one another.

This consistency in hoisting function declarations is a fundamental aspect of JavaScript's design and execution model. It ensures reliable and predictable behavior across various JavaScript engines and environments, aligning with the ECMAScript language standard.

Variable Declarations

On the other hand, variable declarations exhibit a contrasting hoisting behavior. While they are indeed hoisted, only the declaration part is elevated to the top of the current scope. The initialization of variables, however, remains at its original position in the code. This variance in hoisting behavior distinguishes variable declarations from function declarations.

let Doesn't Hoist?

Contrary to common belief, let does hoist, but it exhibits a distinct behavior compared to var. In the case of var, variables are hoisted to the entire function scope and are initialized to undefined at the start of the function's execution. However, when it comes to let, it does hoist, but it doesn't get initialized during the hoisting phase. Instead, it enters what is known as the Temporal Dead Zone (TDZ).

Temporal Dead Zone and TDZ Error

The Temporal Dead Zone (TDZ) is a state where let and const variables exist but remain uninitialized. If you attempt to access a let variable before it's explicitly initialized in your code, JavaScript will throw a TDZ error. This error is a safeguard to ensure that you don't access variables in an unpredictable or incomplete state.

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

TDZ and const

The concept of TDZ primarily arose because of the behavior of const declarations. If const variables were allowed to be accessed before they were assigned a value, it would contradict the core principle of const, which promises immutability and a single assigned value throughout a variable's lifetime. To prevent this, the TDZ mechanism was introduced.

Difference Between var and let/const Hoisting

The critical difference in hoisting behavior is that var variables are initialized to undefined during hoisting. This means that you can access them right away, even if their assignment happens later in the code. On the other hand, let and const variables don't receive an initial value during hoisting. They enter the TDZ, and the only way to bring them out of this state is to explicitly initialize them using the let or const keyword within the block scope. This behavior ensures a clear and consistent approach to variable initialization in JavaScript.

The const keyword works similarly to let in terms of block scoping but with one crucial difference: it declares variables that cannot be reassigned after they are initialized. This can be particularly useful when you want to ensure that a variable retains its value within a block.

Function Expressions

In contrast, function expressions do not exhibit the same behavior. They are not hoisted in the same way as function declarations. When a function is defined through an expression (typically by assigning it to a variable), it's a runtime operation, not something handled during the compilation phase. This means that the variable associated with a function expression is indeed hoisted, but the assignment, which gives it a function value, is not hoisted. Therefore, attempting to call a function expression before it is assigned will result in a TypeError.

sayHello(); // Throws a TypeError
var sayHello = function () {
  console.log("Hello, World!");
};
Enter fullscreen mode Exit fullscreen mode

The reason for this TypeError is straightforward: you're trying to execute a variable that, at that point in the code, holds the value undefined. In contrast, with function declarations, the engine has a plan to initialize them to undefined during compilation, which provides a safety net for calling them before their actual declaration.

In essence, hoisting in JavaScript, whether for function declarations or expressions, is about ensuring that functions are available throughout their scope. However, the way they are initialized and made accessible varies between declarations and expressions, leading to differences in behavior and potential errors like TypeError when calling an undefined function expression.

Conclusion

Understanding hoisting in JavaScript is essential for writing reliable and predictable code. Hoisting, facilitated by the parsing and scope analysis process, ensures that variables and functions are accessible where they should be. It plays a crucial role in differentiating between variable and function declarations, the behavior of let and const, and the handling of function expressions. This knowledge empowers developers to write clean, organized, and error-free JavaScript code, embracing the intricate, yet powerful, mechanism of hoisting.

Sources

Kyle Simpson's "You Don't Know JS"
MDN Web Docs - The Mozilla Developer Network (MDN)

Top comments (0)