Explain the concept of "hoisting" in JavaScript
TL;DR
- Variable declarations (
var
): Declarations are hoisted, but not initializations. The value of the variable isundefined
if accessed before initialization. - Variable declarations (
let
andconst
): Declarations are hoisted, but not initialized. Accessing them results inReferenceError
until the actual declaration is encountered. - Function expressions (
var
): Declarations are hoisted, but not initializations. The value of the variable isundefined
if accessed before initialization. - Function declarations (
function
): Both declaration and definition are fully hoisted. - Class declarations (
class
): Declarations are hoisted, but not initialized. Accessing them results inReferenceError
until the actual declaration is encountered. - Import declarations (
import
): Declarations are hoisted, and side effects of importing the module are executed before the rest of the code.
The following behavior summarizes the result of accessing the variables before they are declared.
Declaration | Accessing before declaration |
---|---|
var foo | undefined |
let foo | ReferenceError |
const foo | ReferenceError |
class Foo | ReferenceError |
var foo = function() { ... } | undefined |
function foo() { ... } | Normal |
import | Normal |
Hoisting
Hoisting is a term used to explain the behavior of variable declarations in your code. Variables declared or initialized with the var
keyword will have their declaration "moved" up to the top of their containing scope during compilation, which we refer to as hoisting.
Only the declaration is hoisted, the initialization/assignment (if there is one), will stay where it is. Note that the declaration is not actually moved – the JavaScript engine parses the declarations during compilation and becomes aware of variables and their scopes, but it is easier to understand this behavior by visualizing the declarations as being "hoisted" to the top of their scope.
Let's explain with a few code samples. Note that the code for these examples should be executed within a module scope instead of being entered line by line into a REPL like the browser console.
Hoisting of variables declared using var
Hoisting is seen in action here as even though foo
is declared and initialized after the first console.log()
, the first console.log()
prints the value of foo
as undefined
.
console.log(foo); // undefinedvar foo = 1;console.log(foo); // 1
You can visualize the code as:
var foo;console.log(foo); // undefinedfoo = 1;console.log(foo); // 1
Hoisting of variables declared using let
, const
, and class
Variables declared via let
, const
, and class
are hoisted as well. However, unlike var
and function
, they are not initialized and accessing them before the declaration will result in a ReferenceError
exception. The variable is in a "temporal dead zone" from the start of the block until the declaration is processed.
y; // ReferenceError: Cannot access 'y' before initializationlet y = 'local';
z; // ReferenceError: Cannot access 'z' before initializationconst z = 'local';
Foo; // ReferenceError: Cannot access 'Foo' before initializationclass Foo {constructor() {}}
Hoisting of function expressions
Function expressions are functions written in the form of variable declarations. Since they are also declared using var
, only the variable declaration is hoisted.
console.log(bar); // undefinedbar(); // Uncaught TypeError: bar is not a functionvar bar = function () {console.log('BARRRR');};
Hoisting of function declarations
Function declarations use the function
keyword. Unlike function expressions, function declarations have both the declaration and definition hoisted, thus they can be called even before they are declared.
console.log(foo); // [Function: foo]foo(); // 'FOOOOO'function foo() {console.log('FOOOOO');}
The same applies to generators (function*
), async functions (async function
), and async function generators (async function*
).
Hoisting of import
statements
Import declarations are hoisted. The identifiers the imports introduce are available in the entire module scope, and their side effects are produced before the rest of the module's code runs.
foo.doSomething(); // Works normally.import foo from './modules/foo';
Under the hood
In reality, JavaScript creates all variables in the current scope before it even tries to executes the code. Variables created using var
keyword will have the value of undefined
where variables created using let
and const
keywords will be marked as <value unavailable>
. Thus, accessing them will cause a ReferenceError
preventing you to access them before initialization.
In ECMAScript specifications let
and const
declarations are explained as below:
The variables are created when their containing Environment Record is instantiated but may not be accessed in any way until the variable's LexicalBinding is evaluated.
However, this statement is a litle bit different for the var
keyword:
Var variables are created when their containing Environment Record is instantiated and are initialized to
undefined
when created.
Modern practices
In practice, modern code bases avoid using var
and use let
and const
exclusively. It is recommended to declare and initialize your variables and import statements at the top of the containing scope/module to eliminate the mental overhead of tracking when a variable can be used.
ESLint is a static code analyzer that can find violations of such cases with the following rules:
no-use-before-define
: This rule will warn when it encounters a reference to an identifier that has not yet been declared.no-undef
: This rule will warn when it encounters a reference to an identifier that has not yet been declared.