2021-09-02
Take a glance over the code snippet below. Without executing the code, what do you think it logs to the console?
var score = 21;
function incrementScore() {
var score = score + 7;
console.log(score);
}
incrementScore(); // ?
If you said NaN
, well done! If not, read on for a quick journey through JavaScript hoisting behavior with variables declared with var
.
First, some background:
Hoisting is not an actual process, rather it is more akin to a mental model that approximates how execution contexts work in JavaScript. From MDN:
JavaScript Hoisting refers to the process whereby the interpreter allocates memory for variable and function declarations prior to execution of the code. Declarations that are made using var are initialized with a default value of undefined. Declarations made using let and const are not initialized as part of hoisting.
Conceptually hoisting is often presented as the interpreter "splitting variable declaration and initialization, and moving (just) the declarations to the top of the code". This allows variables to appear in code before they are defined. Note however, that any variable initialization in the original code will not happen until the line of code is executed.
In effect, JS performs two passes over the code. On the first pass it identifies all of the variable declarations and makes note of their respective scopes. On the second pass (the execution phase, if you will), JS knows what variables exist and where they are in scope. To demonstrate, consider the following code snippet:
console.log(greeting); // undefined
var greeting = "Hello Word!";
console.log(farewell); // ReferenceError!
let farewell = "Until next time..."
The first console.log
invocation returns undefined
because the variable greeting
was declared with var
and therefore initialized with undefined
by default under the hood. The second console.log
invocation throws a reference error because the variable farewell
was declared using the the keyword let
and therefore was not initialized with a default value during the hoisting process.
It's also critical to point out that when it comes to function declarations, JS hoists the entire function declaration, including its body, to the top of its scope. That's why the following code outputs the expected message:
sayHi('George'); // Hey there, George. Welcome!
function sayHi(name) {
console.log(`Hey there, ${name}. Welcome!`);
}
Now, back to the original snippet. A great way to visualize how hoisting works is to re-write the code to mimic the hoisting behavior. For instance:
function addScore() {
var score; // initialized with undefined by default
score = score + 7
console.log(score);
}
var score;
score = 21;
addScore(); // NaN
Here's it's easier to see that the on the second line of the addScore
function body, the expression score + 7
evaluates to undefined + 7
which further evaluates to NaN
. (Note that the outer-scoped variable score
is not accessible inside the function body of addScore
due to variable shadowing, and therefore never comes into play in this example).
Now, if we were to swap out var
for let
, JS throws an error:
var score = 21;
function addScore() {
let score = score + 7;
console.log(score);
}
addScore(); // ReferenceError: Cannot access 'score' before initializtion
In this example, declaring variables with let
results in a very explicit error that will help with debugging. Declaring variables with var
on the other hand results in an ambiguous NaN
, also known as a silent fail.
Ok so at this point I hope that you have a good mental model of hoisting behavior in JS. I'll leave you with a final example. Consider the code below. What will this output to the console?
for (var idx = 0; idx < 2; idx += 1) {
console.log(salutation);
if (idx === 0) {
var salutation = "Hello";
} else {
salutation = "Bye";
}
}
console.log(salutation); // ?
console.log(idx); // ?
The answer is:
undefined
Hello
Bye
2
The critical thing to understand here is that the variable salutation
is declared with var
. This fact has two implications:
salutation
is hoisted to the top of its scope and initialized with undefined
by default and
salutation
has function-scope, or rather, its scope is outside the if
statement block inside which it is defined.
Therefore, on the first loop, console.log(salutation)
logs undefined
, after which the if
block executes and effectively reassigns salutation
to the string Hello
.
On the second loop, console.log(salutation)
logs Hello
, after which the else
block executes and again reassigns salutation
, this time to the string Bye
. After two passes, the loop terminates.
With the loop complete, when console.log(salutation)
is invoked, Bye
is logged to the console thanks to the else
block executing on the second pass of the loop.
console.log(idx)
logs 2
to the console because idx
was incremented after the second loop, which caused the loop conditional to evaluate as false, terminating the loop.