In this lesson, we'll introduce the concept of hoisting, which deals with how function and variable declarations seem to get 'hoisted' to the top of the current scope. We'll also explain how the problems it causes are easily avoided by following simple rules for where and how declarations should happen within your code.
If you read any pre-ES2015 JavaScript materials, hoisting is sure to come up as a topic of concern. However, follow these two simple rules, and you'll never have to worry about it:
- Declare all of your functions at the top of their scope. If the functions are declared in the global scope, simply put them at the top of the JavaScript file. If they're declared inside another function, put the declaration at the top of the function body.
- Only use
const
andlet
. Never usevar
.
- Detail how function and variable declarations are 'hoisted'.
- Explain why it's best to declare functions and variables (at least those declared with
var
) at the top of the scope. - Understand, as always, that it's better to use
const
andlet
thanvar
.
Because the JavaScript engine reads a JavaScript file from top-to-bottom, it would make sense if we had to define a function before we invoked it:
function myFunc () {
return 'Hello, world!';
}
myFunc();
// => "Hello, world!"
However, we can invert those two steps and everything works fine:
myFunc();
function myFunc () {
return 'Hello, world!';
}
// => "Hello, world!"
NOTE: To follow along in your browser's JavaScript console, make sure you type all of the code into the prompt before you press Enter. To insert a new line without executing what you've typed, hold Shift and press Enter. If you type myFunc();
and then hit Enter, the browser will run your code, and you'll see an Uncaught ReferenceError
telling you that myFunc is not defined
. If it helps, you can copy and paste the above code all at once, or you can type it on a single line:
myFunc(); function myFunc () { return 'Hello, world!'; }
// => "Hello, world!"
This reads as though we're invoking the function prior to declaring it, but we're forgetting about the two-phase nature of the JavaScript engine. During the compilation phase, the engine skips right over the invocation and stores the declared function in memory:
// The engine ignores all function invocations during the compilation phase.
myFunc();
function myFunc () {
return 'Hello, world!';
}
By the time the JavaScript engine reaches the execution phase, myFunc()
has already been created in memory. The engine starts over at the top of the code and begins executing it line-by-line:
// During the execution phase, the engine invokes myFunc(), which was already initialized during the compilation phase.
myFunc();
// During the execution phase, the engine will simply ignore this function declaration that was already carried out in the compilation phase.
function myFunc () {
return 'Hello, world!';
}
The term for this process is hoisting because it feels a bit like your declarations are being hoisted to the top of the current scope. Your declarations are being evaluated before the rest of your code gets run, but hoisting is a bit of a misnomer: the physical location of the code isn't actually changing at all.
The best way to avoid any confusion brought on by function hoisting is to simply declare your functions at the very top of your code.
We're going to look at some of the hoisting issues caused by var
because you will encounter this weirdness in legacy code. However, the fix is extremely easy: use const
and let
and you'll have no variable hoisting issues.
Look at the following code:
function myFunc () {
console.log(hello);
var hello = 'World!';
}
// => undefined
Given what you know at this point, what do you think will be logged out to the JavaScript console when the code is executed?
myFunc();
// LOG: undefined
// => undefined
It prints out undefined
. What the heck?!
You see, in JavaScript, hoisting only applies to variable declarations; not variable assignments. As a quick refresher on that terminology:
// Declaration:
let hello;
// Assignment:
hello = 'World!';
// Declaration and assignment on the same line:
let goodnight = 'Moon';
During the compilation phase, the JavaScript engine initializes the variable hello
, storing it in memory. At this point, however, no value is assigned to the variable. As far as the JavaScript engine is concerned, the variable hello
exists, but it contains undefined
.
The variable will contain undefined
until it's assigned a different value during the execution phase. Because of this odd behavior, you'll often see variable hoisting explained by taking some sample code...
function myFunc () {
console.log(hello);
var hello = 'World!';
}
and rearranging it to better indicate the order of events:
function myFunc () {
var hello;
console.log(hello);
hello = 'World!';
}
When rearranged, it's clear that the variable is initialized as undefined
, that it still contains undefined
when it's logged out to the console, and that only after the logging event is it assigned the value of 'World!'
. However, armed with knowledge of what's going on under the hood (the distinct compilation and execution phases), we don't need any of that code transposition nonsense. When we invoke the following function, five things happen:
function myFunc () {
console.log(hello);
var hello = 'World!';
return hello;
}
myFunc();
// LOG: undefined
// => "World!"
- The declaration of
hello
(var hello
) is evaluated during the compilation phase, and the identifier,hello
, is stored in memory asundefined
. - The execution phase starts, and the JavaScript engine begins stepping through the code, executing each line in turn.
- At the first line,
console.log(hello);
, the value ofhello
is stillundefined
, and that's exactly what gets logged out to the console. - At the second line, the value
'World!'
is assigned to the variablehello
. From this point on, all references tohello
in this scope will evaluate to'World!'
. - At the final line, we
return
the value ofhello
, which by now has been assigned and evaluates to'World!'
.
There are two ways to keep the JavaScript engine from 'hoisting' your variables:
-
If, for whatever reason, your current project requires that you use
var
, follow our rule for function declarations and declare everything at the top of its scope. E.g., if you need to declare a variable within a function, declare it at the top of that function:// BAD function myBadFunc () { console.log('Just doing some other stuff before we get around to variable declarations.'); var myVar = 42; } // GOOD function myGoodFunc () { var myVar = 42; console.log("Much better! The variable declaration is at the top of the scope created by 'myGoodFunc()', so there's no chance it gets 'hoisted'."); }
-
For the love of all things good in this world, don't use
var
. Variables declared withconst
andlet
do technically get 'hoisted', but the JavaScript engine doesn't allow them to be referenced before they've been initialized. Bad:myVar; let myVar = "Assignment is optional since we used 'let'."; // ERROR: Uncaught ReferenceError: myVar is not defined
Good:
const myOtherVar = "Gotta assign a value for our beloved 'const'."; myOtherVar; // => "Gotta assign a value for our beloved 'const'."
Since we can't even reference them, the whole problem of hoisted variables evaluating to undefined
prior to assignment is moot.
Hoisting is often cited as an annoyance with JavaScript, but most of those complaints are from a pre-ES2015 world. Rejoice!