DEV Community

Cover image for JavaScript Demystified: Unveiling the Code Wizardry - Hoisting
Guilherme Scotti
Guilherme Scotti

Posted on • Updated on

JavaScript Demystified: Unveiling the Code Wizardry - Hoisting

People commonly explain hoisting as declarations of variables and functions being moved to the top of your code. At the same time, this appears to be happening; it’s essential to understand precisely what is happening. Because that definition is a Myth, a convention created to discuss the idea of lexical environment without so much overhead.

Before the ECMAScript® 2015 Language Specification, "hoisting" was not used in any normative specification.

foo("My Name");

function foo(name) {
  console.log(name);
}
Enter fullscreen mode Exit fullscreen mode

In this case, the function was called before its definition. People often claim that the function is "lifted" to the beginning of the code, but it works due to function prototypes.

function foo(name) {
  console.log(name);
}

foo("My Name");
Enter fullscreen mode Exit fullscreen mode

But that's incorrect, there's no such thing as lifting functions and variables, you see, the code isn’t moving anywhere. It isn’t magically being moved to the top of the file. At the compiler phase, all functions and variable declarations are Hoisted at the Memory Heap, called the Lexical Environment. Then, the Engine will execute the code from top to bottom, adding every scope at the Execution Call Stack.

Hoisting refers to the default behaviour of Javascript to process and put all variables and function declarations into memory first during the compile phase of its execution context, regardless of where they are written in the code. Maya Shavin

Lexical Environment is a data structure with identifier-variable mapping on the memory heap.

Code Compile and Executon

Difficult? Too many concepts? let's make it more simple. A few seconds before your code is executed, the compiler will go through every line, collecting variables and function declarations and storing those in the memory to optimise and utilise even before his declaration in the source code. In other words, the lexical environment is a fancy way to say "local memory scope".

Execution Context animated

Let's see if those concepts can be put into practice.

say("Hello"); // Hello
console.log(world); // world is undefined

function say(word) {
  console.log(word);
}

var world = "World";
Enter fullscreen mode Exit fullscreen mode

So, in this example, the compiler will run through the code and find the function say and save his reference into the memory, making it available to be called in the execution phase, returning 'Hello', but why the world variable is undefined?

JavaScript only hoists variable declarations.
Initializations are not hoisted.

So the variable declaration world will be hoisted at the memory. In the execution phase, the variable is called, but since it isn't initialized yet, the returned value is undefined; here's another example.

if (x === undefined) {
  console.log("x is not defined");
} else {
  console.log(x);
}

var x = "Defined";

//result "x is not defined"
Enter fullscreen mode Exit fullscreen mode

Here the x variable is stored in memory during the compile phase but with the value of undefined, because the assignment of value to a variable happens only in the execution phase.

So what happens is something like this before the execution phase

var x;

if (x === undefined) {
  console.log("x is not defined");
} else {
  console.log(x);
}

x = "Defined";

//result "x is not defined"
Enter fullscreen mode Exit fullscreen mode

Let's see what happens if we swap the declarations of the variable 'x'

var x = "Defined";

if (x === undefined) {
  console.log("x is not defined");
} else {
  console.log(x);
}

//result "Defined"
Enter fullscreen mode Exit fullscreen mode

This time, the variable x will be hoisted/lifted by the compiler with the value of undefined, but when the execution phase starts, the x variable will be evaluated at the first line with the value of Defined making possible to run the else condition.

There's also a lot of confusion about undefined and undeclared variables (Reference Error). When a variable is undefined, it means that the variable has been declared at the compiler phase but not initialized, and undeclared are variables that weren't declared in the lexical environment or the JS Engine doesn't find an available reference for them.

What happens with ES6 syntax?

So, what happens with all the features of ES6, like let and const? Are they hoisted? let's see

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

let x = "Hello";
Enter fullscreen mode Exit fullscreen mode

All declarations in JavaScript function, var, let, const, even classes, are hoisted at the compiler phase, but while the var declarations are initialized with undefined, let and const declarations remain uninitialized, so where it's at the first line of the example? he was in a place that we call "Temporal Dead Zone".

TDZ

No, nothing like that, let's see what ECMAScript 2015 spec tells us about TDZ.

NOTE 13.2.1 Let and const declarations define variables scoped to the running execution context’s Lexical Environment. The variables are created when their containing Lexical Environment is instantiated but may not be accessed in any way until the variable’s LexicalBinding is evaluated. A variable defined by a LexicalBinding with an Initializer is assigned the value of its Initializer’s AssignmentExpression when the LexicalBinding is evaluated, not when the variable is created. If a LexicalBinding in a let declaration does not have an Initializer, the variable is assigned the value undefined when the LexicalBinding is evaluated.

In other words, let and const are only initialized when their assignment of value is evaluated, and that happens during the execution phase, so in the example, the x variable will be evaluated only after called by console.log, that's why throws a ReferenceError.

Now, let's see what happens when we introduce the block scope.

var name = "Guilherme";
displayName();
function displayName() {
  console.log(name);
  // ReferenceError meaning TDZ error
  let name = "Scotti";
}
Enter fullscreen mode Exit fullscreen mode

Now, the global scope has a name variable with the value of 'Guilherme', but when the Engine enters the block scope, it creates another execution context and hoists the variable name into the TDZ of this block context. So, when the console.log runs, the variable name is unreachable.

To summarise, TDZ means don't touch that until it is initialized.

don't touch that

The whole point of the TDZ is to make it easier to catch errors where accessing a variable before it’s declared in user code leads to unexpected behaviour. This happened a lot with ES5 due to hoisting and poor coding conventions. In ES6 it’s easier to avoid.

Function Expressions or Declarations

There are three ways to define a function in JavaScript: a function declaration (assigning the function value into a variable), function expression, and ES6 Arrow Functions. So, are they all Hosted equally?

hello(); // "Hello"
world(); // TypeError: world is not a function
exclamation();
// ReferenceError: exclamation is not defined

function hello() {
  console.log("Hello");
}
var world = function() {
  console.log("World");
};
const exclamation = () => console.log("!");
Enter fullscreen mode Exit fullscreen mode

We already know that Function Declarations are Hoisted during the compile phase, so the hello() function is ready to be called at line 1 because it's a function declaration.

At line 2, we call the function world(), but actually, we are calling the variable world that was hoisted with the same rule as any other variable with the default value of undefined. Therefore, when the execution reached line 2 and tried to call world(), it failed returning a TypeError, because undefined is not a function!

That also happens with the arrow function, but with a little difference: ES6 Syntax is initialized only at value evaluation on the execution phase, so there's the Reference error because of the const declaration.

Conclusion

This is an important foundation in the understanding of how JavaScript works under the hood, and the main conclusion that can be drawn is that all declarations in JavaScript function, var, let, const, even classes, are hoisted at the compiler phase, but with different assignments of value. Variables are hoisted with the value of undefined, except const and let, which are only initialized when their assignment of value is evaluated, and that happens during the execution phase; before that, they are in TDZ.

We also saw that function declarations are hoisted and evaluated normally, but function expressions are treated precisely like variables. I hope you guys can glimpse some functionalities that run on our daily code in JS.

Now that you know more about Hoisting, it’s time to move to dive Deeper with Javascript Demystified Series.

If you want to dive deeper into some concepts, visit the References. Please post any feedback, questions, or requests for topics. I would also appreciate 👏 if you like the post so that others can find this too.

Top comments (1)

Collapse
 
tnfe profile image
tntweb team

cool