DEV Community

Matthew Aftalion
Matthew Aftalion

Posted on • Edited on

Beyond the Basics: Hoisting

Most JavaScript developers nowadays have heard the term "hoisting" but are usually still fuzzy on what's actually happening behind the scenes. Good programming conventions and "strict mode" help prevent many bugs and errors that result from hoisting but it's always beneficial to understand why those conventions were standardized.

What this article covers:

  • What hoisting is and how it affects your code
  • How improper understanding of hoisting can lead to undefined values or errors
  • Function declaration vs expression
  • Var vs Let/Const in terms of hoisting

What is Hoisting?

Hoisting is usually defined as when variable/function declarations are "lifted" to the top of their lexical scope. This leads to the misconception that these declarations are physically moved to the top of your code, but what really happens is that your variable/function declarations are put into memory during the compile phase but really stay exactly where they were defined in your code.

Function Declarations

Two of the most common ways to define a function are through declarations and expressions.

Normally we tend to define a function before we use it:

function printName(name) {
  console.log(name)
}

printName('Matt'); // logs 'Matt'

But if we use a function declaration, the function will be put into memory during compilation and will be available in that lexical scope before even reaching that line of code during execution:

printName('Matt'); // same scope as function definition, still logs 'Matt'

function printName(name) {
  console.log(name)
}

Although the function is available in memory, it's only available in that same function scope:

printName('Matt'); // global scope, throws error
// 'Uncaught ReferenceError: printName is not defined'

(function() { // creates a function scope
  printName('Matt'); // logs 'Matt'

  function printName(name) { // only available in this function scope
    console.log(name)
  }
})()

Only function declarations are hoisted, trying to invoke a function expression before it's executed will result in an error:

test('Matt'); // Uncaught TypeError: test is not a function

var test = function(name) {
  console.log(name);
}

But wait...why did we get a TypeError? Shouldn't we have gotten a ReferenceError since it's being called before it's defined?

The answer to this mystery will become clear in the next section.

Variable Declarations: Var vs Let/Const

Declarations using 'Var'

Variable declarations using 'var' work similar to function declarations in that they also get hoisted.

So why did the function invocation in the section above lead to a TypeError and not a ReferenceError?

This is because a function expression is basically a variable declaration and an assignment to a function in the same step. The variable gets hoisted and remains as undefined until it reaches that line of code during execution.

// 1. variable declaration & assignment in same line
var printName = function() { ... } // declare printName and assign to a function

// 2. Same as above but in two separate steps
var printName; // declare printName
// printName gets hoisted and set as undefined until it reaches the line below
printName = function() { ... } // assign to a variable

Now lets revisit our previous example:

// var test was hoisted and is undefined
test('Matt'); // throws a TypeError since we're trying to invoke 'undefined'

var test = function(name) { // now test is assigned to a function
  console.log(name);
}

test('Matt') // logs 'Matt'

Just to make sure you've understood variable declarations, what would the following code output?

console.log(name);
var name;
name = 'Matt';

If you guessed undefined then give yourself a pat on the back! But what happens if we assign the variable before we declare it? Is it still going to be function scoped or will it be set to our global scope?

name = 'Matt'; // global scope or function scope?
console.log(name); // logs 'Matt'
var name;

If a variable is declared in the same scope it will be assigned to that variable, otherwise it will go up the scope chain until it reaches the global scope and is set there.

(function() {
  name = 'Matt'; // since no 'name' var was declared in this function scope it will move up the scope chain
})()
console.log(name); // logs 'Matt'
(function() { // name was declared and got hoisted
  name = 'Matt'; // assign 'Matt' to the name variable in this function scope
  var name;
})()
console.log(name); // name was scoped to the function above and is not available outside that scope
// Uncaught ReferenceError: name is not defined

Declarations using 'Let/Const'

One main difference between let/const and var is that they are block scoped whereas 'var' is function scoped. But how are they different in terms of hoisting?

Well for one, since const can't be re-assigned, it will always have to be assigned when it's declared:

const name = 'Matt'; // Only way to define a constant variable

const anotherName; // throws a error, SyntaxError: Missing initializer in const declaration
anotherName = 'Matt'; 

Wait but earlier we said that variable declarations get hoisted and set as 'undefined' until we reach the assignment during execution, so doesn't that mean we're re-assigning constant variable?

No, because let/const variables don't get hoisted.

// Const have to be declared before they can be used
console.log(name); // ReferenceError: Cannot access 'name' before initialization
const name = 'Matt'; 

// Let also has to be declared before it can be used
console.log(name); // ReferenceError: Cannot access 'name' before initialization
let name = 'Matt'; 

Back to our previous example using 'var', if we swap it out for 'let' it will no longer get hoisted and will throw an error.

(function() { // name was not hoisted
  name = 'Matt'; // ReferenceError: Cannot access 'name' before initialization
  let name;
})()
console.log(name);

Summary

Hoisting can lead to many bugs or unintended behavior if not understood well. Two ways to avoid some damage that hoisting might cause is:

  1. Always declare your variables at the top of each scope.
  2. Using Let/Const over Var can help prevent bugs due to hoisting
  3. Use strict mode to prevent variables from being used if they are not declared.

What we learned

  • Hoisting doesn't physically move any code, it just places function/variable declarations in memory.
  • Function declarations get hoisted and are available during execution in the same scope as where the function was declared.
  • Variables defined using 'var' get hoisted.
  • Variables defined using 'let/const' do not get hoisted.

More Resources

https://www.w3schools.com/js/js_hoisting.asp
https://developer.mozilla.org/en-US/docs/Glossary/Hoisting

Top comments (0)