First published on thejs.dev → https://thejs.dev/jmitchell/what-is-hoisting-and-how-it-works-in-javascript-e1e
The famed and often confusing term, certainly for any JavaScript developer, and it leaves many scratching their heads; what exactly is hoisting?
Hoisting is a term that describes a mechanism in JavaScript to provide early-access to declarations.
Hoisting is a JavaScript mechanism where variables and function declarations are moved to the top of their scope before code execution.
Ever wondered why in JavaScript you can interact with your variables and functions before they've been declared? Then read on!
tl;dr
Conceptually speaking, hoisting is the movement of declaractions - variables and functions - to the top of their scope, while assignments are left in place. What really happens, is that during compile time, the declarations are put into memory first, but physically remain in place in your code.
The benefit of doing this is that you get access to functions and variables before they're declared in code. This only applies to declarations, not to expressions, and not to initializing an undeclared variable.
foo("bar");
function foo(bar) {
// do something
}
It's important to remember that only declarations are hoisted into memory during compile time, not assignments.
Only declarations are hoisted
JavaScript only hoists the declarations of functions and variables into memory during compile time, not the assignments. This means that if a variable is declared and initialized after using it, the value will be undefined
.
console.log(foo) // prints 'undefined' as only the _declaration_ was hoisted
var foo; // declaration
foo = "bar"; // initialization
The following is an example of initialization returning a ReferenceError
. In this case, a variable is only initialized, not declared; in order to declare something to be hoisted in JavaScript, it must either be explicitly declared as a var or function, as implicit declaractions (initialization only) wont be hoisted.
console.log(foo); // prints 'ReferenceError: foo is not defined'
foo = "bar"; // only initialization, no declaration, value did not follow var
It's important to remember that only the declaration is hoisted, even if a value is assigned.
console.log(foo); // prints 'undefined'
var foo = "bar";
Hoisting variables
JavaScript allows us to declare and initialize our variables simultaneously. However, the engine separates the declaration and initialization of variables, and executes it as separate steps, thus allowing the engine to hoist declarations above initializations.
All function and variable declarations are hoisted to the top of their scope, while variable declarations are processed ahead of function declarations; which is why you can call functions with yet-to-be-declared variables, and get an undefined
error.
However, there is a caveat. When initializing a variable, that hasn't yet been declared yet, the variable is hoisted to the global scope when it is executed, rather than hoisted to its scope, like the function it's being initialized in. This only happens on execution, not at compile time.
function doSomething() {
doing = "something";
}
console.log(doing); // ReferenceError: doing is not defined
doSomething();
console.log(doing); // something
This is distinctly different to scoped variable declarations, which only exist within their scope.
function doSomething() {
var doing = "something";
}
console.log(doing); // ReferenceError: doing is not defined
doSomething();
console.log(doing); // ReferenceError: doing is not defined
The take-away is, that declared variables are hoisted to the top of their scope at compiled time, while undeclared variables are hoised to the global scope during execution.
Declarations using let
and const
are not hoisted to global space
let
and const
were introduced in ES6 for scope-based operations, but unlike var
, do not get hoisted to global space during compile time. Variables declared with let
are block scoped and not function scoped. This is significant, because unlike var
, there's no risk of variable leakage outside of the scope of execution.
The downside is that const
and let
do not get hoisted, in local or global scope. Read more about var, const and let.
console.log(foo); // prints 'ReferenceError: foo is not defined'
const foo = "bar";
console.log(bar); // prints 'ReferenceError: bar is not defined'
let bar = "bar";
Strict mode prevents sloppy hoisting
Introduced as a utility in ES5, strict-mode is a way to opt in to a restricted variant of JavaScript, implicitly opting-out of sloppy mode. It introduces different semantics, such as eliminating some silent errors, improves some optimizations and prohibits some syntax, such as accessing variables before they've been declared.
You can opt-in to strict-mode by pre-facing your file, or function with use strict
at the top of the scope, before any code is declared:
'use strict';
// or
"use strict";
We can test if we can access initializations ahead of time with strict-mode enabled:
"use strict";
function doSomething() {
foo = 20;
}
doSomething();
console.log(foo); // prints 'ReferenceError: foo is not defined'
Not all functions are hoisted alike
Function are classified as either function declarations, or function expressions. The important difference between the two, when discussing hoisting, is declaration. A declared function will be hoisted, while a function created through an expression will not be hoisted.
console.log(typeof hoistedFunction); // prints 'function'
console.log(typeof unhoistedFunction); // prints 'undefined'
function hoistedFunction() {
// This function _will_ hoisted, because it is *declared* as a function
}
var unhoistedFunction = function() {
// This function _will not_ be hoisted because it is declared through an expression of a variable, and therefore will be undefined
}
Order or hoisting precedence
There are two rules you have to keep in mind when working with hoisted functions and variables:
- Function declaration takes precedence over variable declarations
- Variable assignment takes precedence over expression function
console.log(typeof myVar); // prints 'undefined'
console.log(typeof myFunc); // prints 'function'
var myVar = "foo"; // declaration and initialization
function myFunc(){} // declaration
console.log(typeof myVar); // prints 'string'
We can take a deeper look at the steps during the compilation and execution cycle to see what's happening with our declaration and initializations:
function myFunc(){...}
var myVar;
console.log(typeof myVar);
console.log(typeof myFunc);
myVar = "foo";
console.log(typeof myVar);
Classes are not hoisted
Classes in JavaScript are in fact special functions, and just as you can define functions with declaration and expression, the class syntax has the same two components: class expressions and class declarations.
Unlike functions and variables, classes do not get hoisted, either through declaration or expression. You need to declare your class before you can use it.
const p = new Rectangle(); // ReferenceError
console.log(typeof Rectangle); // ReferenceError
class Rectangle {}
Conclusion
Let's summarise what we've learned:
- Hoisting is a mechanism that inserts variable and function declaractions into memory ahead of assignment and initialization within the given scope of execution
-
const
,let
, function expressions and classes do not get hoisted, and cannot be read or accessed before their declaration -
safe-mode
prevents sloppy hoisting of initialized undeclared variables onto the global scope
To avoid hoisting confusion and issues in the longterm, it's better to declare your variables and functions ahead of initialization and access. You'll avoid a whole set of potentially nasty bugs and undefined
warnings polluting your console.
Top comments (0)