Hoisting

faminiprodev

Famini-ProDev

Posted on April 23, 2022

Hoisting

By now, you should be fairly comfortable with the idea of scope, and how variables are attached to different levels of scope depending on where and how they are declared. Both function scope and block scope behave by the same rules in this regard: any variable declared within a scope is attached to that scope.
But there’s a subtle detail of how scope attachment works with declarations that appear in various locations within a scope, and that detail is what we will examine here

Chicken or the Egg?
There’s a temptation to think that all of the code you see in a JavaScript program is interpreted line-by-line, top-down in order, as the program executes. While that is substantially true, there’s one part of that assumption that can lead to incorrect thinking about your program.
Consider this code:

a = 2;

var a;

console.log( a );
Enter fullscreen mode Exit fullscreen mode

What do you expect to be printed in the console.log(..) statement?
Many developers would expect undefined, since the vara statement comes after the a = 2, and it would seem natural to assume that the variable is redefined, and thus assigned the default undefined. However, the output will be 2.
Consider another piece of code:

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

You might be tempted to assume that, since the previous snippet exhibited some less-than-top-down looking behavior, perhaps in this snippet, 2will also be printed. Others may think that since the a variable is used before it is declared, this must result in a ReferenceErrorbeing thrown.
Unfortunately, both guesses are incorrect. undefined is the output.
The Compiler Strikes Again
the best way to think about things is that all declarations, both variables and functions, are processed first, before any part of your code is executed.
When you see var a = 2;, you probably think of that as one statement. But JavaScript actually thinks of it as two statements: var a; and a = 2;. The first statement, the declaration, is processed during the compilation phase. The second statement, the assignment, is left in place for the execution phase.
Our first snippet then should be thought of as being handled like this:

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

…where the first part is the compilation and the second part is the execution.
Similarly, our second snippet is actually processed as:

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

So, one way of thinking, sort of metaphorically, about this process, is that variable and function declarations are “moved” from where they appear in the flow of the code to the top of the code. This gives rise to the name hoisting.
In other words, the egg (declaration) comes before the chicken (assignment).

NOTE

Only the declarations themselves are hoisted, while any assignments or other executable logic are left in place. If hoisting were to re-arrange the executable logic of our code, that could wreak havoc.

foo();
function foo() {
    console.log( a ); // undefined
    var a = 2;
}
Enter fullscreen mode Exit fullscreen mode

The function foo’s declaration (which in this case includes the implied value of it as an actual function) is hoisted, such that the call on the first line is able to execute.
It’s also important to note that hoisting is per-scope. So while our previous snippets were simplified in that they only included global scope, the foo(..) function we are now examining itself exhibits that vara is hoisted to the top of foo(..) (not, obviously, to the top of the program). So the program can perhaps be more accurately interpreted like this:

function foo() {
    var a;
    console.log( a ); // undefined
    a = 2;
}
foo();
Enter fullscreen mode Exit fullscreen mode

Function declarations are hoisted, as we just saw. But function expressions are not.

foo(); // not ReferenceError, but TypeError!
var foo = function bar() {
    // ...
};
Enter fullscreen mode Exit fullscreen mode

The variable identifier foo is hoisted and attached to the enclosing scope (global) of this program, so foo() doesn’t fail as a ReferenceError. But foo has no value yet (as it would if it had been a true function declaration instead of expression). So, foo() is attempting to invoke the undefined value, which is a TypeErrorillegal operation.
Also recall that even though it’s a named function expression, the name identifier is not available in the enclosing scope:

foo(); // TypeError
bar(); // ReferenceError
var foo = function bar() {
    // ...
};
Enter fullscreen mode Exit fullscreen mode

This snippet is more accurately interpreted (with hoisting) as:

var foo;
foo(); // TypeError
bar(); // ReferenceError
foo = function() {
    var bar = ...self...
    // ...
}
Enter fullscreen mode Exit fullscreen mode

Functions First

Both function declarations and variable declarations are hoisted. But a subtle detail (that can show up in code with multiple “duplicate” declarations) is that functions are hoisted first, and then variables.
Consider:

foo(); // 1
var foo;
function foo() {
    console.log( 1 );
}
foo = function() {
    console.log( 2 );
};
Enter fullscreen mode Exit fullscreen mode

1is printed instead of 2! This snippet is interpreted by the Engine as:

function foo() {
    console.log( 1 );
}
foo(); // 1
foo = function() {
    console.log( 2 );
};
Enter fullscreen mode Exit fullscreen mode

Notice that varfoo was the duplicate (and thus ignored) declaration, even though it came before the function foo()… declaration, because function declarations are hoisted before normal variables.
While multiple/duplicate vardeclarations are effectively ignored, subsequent function declarations do override previous ones.

foo(); // 3
function foo() {
    console.log( 1 );
}
var foo = function() {
    console.log( 2 );
};
function foo() {
    console.log( 3 );
}
Enter fullscreen mode Exit fullscreen mode

While this all may sound like nothing more than interesting academic trivia, it highlights the fact that duplicate definitions in the same scope are a really bad idea and will often lead to confusing results.
Function declarations that appear inside of normal blocks typically hoist to the enclosing scope, rather than being conditional as this code implies:

foo(); // "b"
var a = true;
if (a) {
   function foo() { console.log("a"); }
}
else {
   function foo() { console.log("b"); }
}
Enter fullscreen mode Exit fullscreen mode

However, it’s important to note that this behavior is not reliable and is subject to change in future versions of JavaScript, so it’s probably best to avoid declaring functions in blocks.

Review

We can be tempted to look at var a = 2; as one statement, but the JavaScript engine does not see it that way. It sees vara and a = 2 as two separate statements, the first one a compiler-phase task, and the second one an execution-phase task.
What this leads to is that all declarations in a scope, regardless of where they appear, are processed first before the code itself is executed. You can visualize this as declarations (variables and functions) being “moved” to the top of their respective scopes, which we call hoisting.
Declarations themselves are hoisted, but assignments, even assignments of function expressions, are not hoisted.
Be careful about duplicate declarations, especially mixed between normal vardeclarations and function declarations—peril awaits if you do!

Reference
You Don’t Know JS: Kyle Simpson Quotes

💖 💪 🙅 🚩
faminiprodev
Famini-ProDev

Posted on April 23, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related