/

dronkers.dev

dronkers.dev logo

Hoisting Chaos

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:

  1. salutation is hoisted to the top of its scope and initialized with undefined by default and

  2. 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.