JavaScript: Hoisting
Posted by in Programming onVariables are a pretty basic component of just about any programming language, and JavaScript is no exception:
var myVariable = 'Hello, there!';
But since JavaScript is... well... JavaScript... there's a quirk here that creates some pretty unusual behavior if you aren't prepared for it. Hoisting is a process that, in a nutshell, moves variables and functions to the top of their scope.
Variables
The lifecycle for a JavaScript variable can be roughly described in three parts: declaration, assignment, and access. All three of these steps should be included.
When a scope is created, JavaScript will scan the top-level for var
and function
declarations and assigns memory for those variables. In a practical sense, this behavior makes JavaScript parse scopes as if all of the function
and var
declarations were on the first line of the new scope:
function hoist() {
a = true; // assignment
console.log(a); // access
var a; // declaration
}
// is equivalent to
function hoist() {
var a; // declaration
a = true; // assignment
console.log(a); // access
}
Like many languages, JavaScript also allows you to perform the declaration and assignment steps on one line. However, they will be split into two steps, regardless — the assignment will not be hoisted:
function hoist() {
a = true; // assignment
console.log(a); // access: true
var a = false; // declaration & assignment
console.log(a); // access: false
}
// is equivalent to
function hoist() {
var a; // declaration
a = true; // assignment
console.log(a); // access: true
a = false; // assignment
console.log(a); // access: false
}
Some Caveats
1. This only works with ECMAScript 5 variable declarations.
When working with variables, only those declared with var
are affected. Variables declared with let
and const
are relatively new to JavaScript and follow different rules.
2. You should declare variables before assigning them.
If a variable is assigned before it is declared, it will be implicitly moved to the global scope. This behavior can result in bugs that are difficult to track, especially in a large project.
function createsGlobal() {
a = true; // assignment (without declaration)
}
createsGlobal();
console.log(a); // access: true
// is equivalent to
var a; // implicit declaration
function createsGlobal() {
a = true; // assignment
}
createsGlobal();
console.log(a); // access: true
You can address this problem by adding 'use strict'
or "use strict"
to the top of the scope. It is also considered a best-practice to declare variables at the top of a function since this is how JavaScript already interprets your code.
function noGlobals() {
'use strict';
b = true; // ReferenceError: b is not defined
}
function noErrors() {
var b, c, d;
b = true;
c = true;
d = false;
return { b, c, d };
}
3. You should declare variables before accessing them.
If a variable is accessed before it is declared, it will throw a reference error.
x; // ReferenceError: x is not defined
You can prevent this by declaring a variable at the most specific scope possible:
function getCounter() {
var x = 0;
return {
increment() { return ++x; },
decrement() { return --x; },
};
}
Functions
Function declarations and definitions follow similar rules to variables. Like variables, their declarations are hoisted. Unlike variables, their assignments are also hoisted.
hoisted();
function hoisted() {
console.log('Hoisted!');
}
// is equivalent to
function hoisted() {
console.log('Hoisted!');
}
hoisted();
Variables and Functions with the same name
Let's say you find code that looks something like this:
var x = true;
function x() {
// do stuff
}
console.log(x);
How would the JavaScript engine interpret this? The answer is surprisingly simple: hoisting is done in batches.
First, JavaScript will hoist the variable declarations. If we move both declarations to the top of the scope, our example looks like this:
var x, x; // declarations are moved to the top of the scope
// ---
x = true;
x = function() {
// do stuff
};
console.log(x);
Since x
is declared twice, one declaration can be ignored, which leaves us with this:
var x;
// ---
x = true;
x = function() {
// do stuff
}
console.log(x);
Second, JavaScript will hoist the function definition. This is what really separates functions from variables. Function definitions are evaluated right after hoisting occurs:
var x;
x = function() { // hoisted
// do stuff
}
// ---
x = true;
console.log(x); // outputs "true"
This has the rather odd side-effect of functions being overwritten by local variables, regardless of where they were defined in a scope.
One major exception to the rule: if a function is explicitly declared as a variable, then the assignment will not be hoisted.
hoisted(); // TypeError: expression is not a function
var hoisted = function() {
console.log('Not Hoisted!');
};
// is equivalent to
var hoisted;
hoisted();
hoisted = function() {
console.log('Not Hoisted!');
};
This is a relatively common practice, so it's important to be aware of the difference between the two styles.
Classes
Classes are a special case, in that they are treated as functions but follow the same rules as a const
declaration and assignment — they are not hoisted, and they cannot be reassigned. This, coupled with the lack of Internet Explorer support, means that classes are less common in browser-side JavaScript than in Node.js. Given what has already been covered, these two examples should demonstrate the lack of hoisting on classes:
console.log(typeof NotHoisted); // ReferenceError: NotHoisted is not defined
class NotHoisted {}
class ForwardDeclared {}
console.log(typeof ForwardDeclared); // function
Conclusion
Whenever a new scope is declared, three steps are immediately evaluated:
- Variable declarations are moved to the top of the scope.
- Function definitions are assigned to variables that share their name.
- Code within the scope is executed, including assignments and accesses.
However:
- Variables declared with
const
,let
orclass
are not hoisted. - Overwriting a function's variable will overwrite the function, even if the function is declared later.
- Function assignment is only hoisted if it is not explicitly assigned to a variable.
Some habits you should build:
- Put
"use strict"
or'use strict'
at the top-level scope to catch subtle bugs. - Move
var
declarations to the top of their scope. - Avoid using the same name for a variable and a function.
If you would like to learn more about how scope works in JavaScript — including the const
, let
, and class
declarations — you may be interested in reading the JavaScript: Scope entry! Good luck!