const - JS | lectureHow JavaScript Works Behind the Scenes

Variable Environment - Hoisting and The TDZ

How JavaScript Works Behind the Scenes

In this lecture, we will talk about a very misunderstood concept in JavaScript, hoisting.

We learned that an execution context always contains three parts: a variable environment, the scope chain in the current context, and the this keyword. We already learned about the scope chain, and so now it's time to take a closer look at the variable environment and, in particular, at how variables are actually created in JavaScript.

In JavaScript, we have a mechanism called hoisting. It makes some types of variables accessible or usable in the code before they are declared. Many people define hoisting by saying that variables are magically lifted or moved to the top of their scope. That's actually what hoisting looks like on the surface, but behind the scenes, that's not what happens.

Instead, behind the scenes, the code is scanned for variable declarations before it is executed. This happens during the so-called creation phase of the execution context that we talked about before. Then for each variable that is found in the code, a new property is created in the variable environment object. That's how hoisting really works.

Now, hoisting does not work the same for all variable types. Let's analyze how hoisting works for function declarations, variables defined with var, variables defined with let or const, function expressions, and arrow functions.

Hoisting - Comparison Table

FUNCTION DECLARATIONS

Function declarations are hoisted, and the initial value in the variable environment is set to the actual function. In practice, what this means is that we can use function declarations before they are actually declared in the code because they are stored in the variable environment object, even before the code starts executing.

Hoisting - Function Declaration

VARIABLES DECLARED WITH VAR

Next, variables declared with var are also hoisted, but hoisting works differently here. Unlike functions, when we try to access a var variable before it's declared in a code, we don't get the declared value, we get undefined.

This is a really weird behavior for beginners. You might expect to get an error when using a variable before declaring it or getting the actual value but not undefined because having undefined is weird and not useful either.

This behavior is a common source of bugs in JavaScript and is one of the main reasons we rarely use var in modern JavaScript.

Hoisting - VAR Variables

LET AND CONST VARIABLES

Variables declared with let and const are not hoisted. Technically they are hoisted, but their value is set to uninitialized. It's like there is no value to work with at all, and so in practice, it is as if hoisting was not happening.

Instead, we say that these variables are placed in a so-called Temporal Dead Zone or TDZ, making the variables between the beginning of the scope and where the variables are declared inaccessible.

Consequently, we get an error if we attempt to use a let or const variable before it's declared. Also, keep in mind that let and const are block-scoped. They exist only in the block in which they were created.

All these factors together are basically the reason why let and const were first introduced into the language and why we use them now instead of var in modern JavaScript.

Hoisting - LET and CONST Variables

FUNCTION EXPRESSIONS AND ARROW FUNCTIONS

The way hoisting works for function expressions, and arrow functions depends on whether they were created using var, const or let because keep in mind that these functions are simply variables. So, they behave the same way as variables regarding hoisting.

This means that a function expression or arrow function created with var is hoisted to undefined. But if created with let or const, it's not usable before it's declared in a code because of the Temporal Dead Zone.

This is the reason why I told you earlier that we cannot use function expressions before we write them in the code, unlike function declarations.

Hoisting - Function Expressions and Arrow Functions

Before finishing, let's take a more detailed look at this mysterious Temporal Dead Zone.

script.js
const user = 'Mike';

if (user === 'Mike') {
  console.log(`Mike is a ${job}`);
  const age = 2022 - 1989;
  console.log(age);
  const job = 'Software Engineer';  console.log(me);
}

In the above example code, we will look at the job variable. It is a const, so it's scoped only to the if block, and it's going to be accessible starting from the line where it's defined.

const user = 'Mike';

if (user === 'Mike') {
  console.log(`Mike is a ${job}`);  const age = 2022 - 1989; // TDZ  console.log(age);  const job = 'Software Engineer';
  console.log(me);
}

The reason is, as you can see above (The highlighted section), there is a Temporal Dead Zone for the job variable. It's the region of the scope in which the variable is defined but can't be used. It is as if the variable didn't even exist.

Now, if we still try to access the variable while in the TDZ we get a reference error telling us that we can't access job before initialization, just as we learned.

Hoisting - TDZ Error

However, if we tried to access a variable that was never even created, like in the last line where we want to log me, we get a different error message saying that me is not defined.

This means that job is, in fact, in the Temporal Dead Zone where it is still uninitialized. Still, the engine knows it will eventually be initialized because it already read the code before and set the job variable in the variable environment to uninitialized. Then when the execution reaches the line where the variable is declared, it is removed from the Temporal Dead Zone, and it's then safe to use.

To recap, every let and const variable gets its own Temporal Dead Zone that starts at the beginning of the scope until the line where it is defined, and the variable is only safe to use after the TDZ.

Now, what is the need for JavaScript to have a Temporal Dead Zone? Well, the main reason that the TDZ was introduced in ES6 is that the behavior I described before makes it way easier to avoid and catch errors because using a variable that is set to undefined before it's actually declared can cause serious bugs, which might be hard to find. We are going to see some examples in the next lecture.

Accessing variables before the declaration is bad practice and should be avoided. The best way to avoid it is by simply getting an error when we attempt to do so, and that's exactly what a Temporal Dead Zone does.

Another reason why the TDZ exists is to make const variables actually work the way they are supposed to. As you know, we can't reassign const variables, making it impossible to set them to undefined first and then assign their real value later.

const should never be reassigned. And so, it's only assigned when execution actually reaches the declaration, which makes it impossible to use the variable before.

Now, if hoisting creates so many problems, why does it exist in the first place? The creator of JavaScript implemented hoisting to use function declarations before actual declaration because this is essential for some programming techniques, such as mutual recursion. Some people also think that it makes code a lot more readable.

Now, the fact that it also works for var declarations is because that was the only way hoisting could be implemented at the time. So the hoisting of var variables is basically just a byproduct of hoisting functions, and it probably seemed like a good idea to simply set variables to undefined, which in hindsight is not really that great.

However, we need to remember that JavaScript was never intended to become the huge programming language that it is today. Also, we can't remove this feature from the language now, and so we just use let and const to work around this.