Don't let
Hoisting Put Your Variables Up For Grabs
JavaScript has a curious behavior if you're coming from another
programming language. (Okay, JavaScript has
Variables declared outside of any functions are global variables that can be referenced from any JavaScript code. Variables declared within a function are local variables and can only be referenced from with the function that declared them or from within scopes that are nested within the function that declared them.
var thisIsGlobal = "global"; function doSomething() { var thisIsLocal = "local"; console.log(thisIsGlobal); // okay console.log(thisIsLocal); // okay } console.log(thisIsGlobal); // okay console.log(thisIsLocal); // 'thisIsLocal' is not defined here
These rules are straight-forward and consistent with most programming languages. Here's where things can get confusing.
function monitorSleep() { console.log(sleepHours); // What happens here? }
When you call this function, you'll see the following output in the console window:
Uncaught ReferenceError: sleepHours is not defined at monitorSleep (:2:17) at :1:1
That's understandable. We forgot to declare the sleepHours
variable, so let's add it.
function monitorSleep() { console.log(sleepHours); // What happens here? var sleepHours = 0; }
Now we've stumbled upon that curious behavior. We're referencing
our sleepHours
variable before we declare it,
but instead of receiving the earlier error message, the console
window reports:
undefined
If we duplicate the logging instruction after declaring the variable, we see this output:
undefined 0
Hoisting
This behavior is caused by hoisting. When JavaScript parses any code, the parser makes a first pass through the code and locates any variable or function declarations. Those declarations are then "hoisted" to the top of their current scope. So, let's say we have this code:
var a = 1; var b = 2; function doMath(num1, num2) { var sum = num1 + num2; console.log(sum); var diff = num1 - num2; console.log(diff); var prod = num1 * num2; console.log(prod); var quot = num1 / num2; console.log(quot); return Math.max(sum, diff, prod, quot); } var c = doMath(a, b); console.log(c);
The JavaScript interpreter will understand it as:
var a = undefined; var b = undefined; var c = undefined; function doMath(num1, num2) { var sum = undefined; var diff = undefined; var prod = undefined; var quot = undefined; sum = num1 + num2; console.log(sum); diff = num1 - num2; console.log(diff); prod = num1 * num2; console.log(prod); quot = num1 / num2; console.log(quot); return Math.max(sum, diff, prod, quot); } a = 1; b = 2; c = doMath(a, b); console.log(c);
Notice all the variable declarations have been moved to the top
of their scope, whether global or
local, and initialized as
undefined
to the console window when we issued the
console.log
statement before declaring the variable.
JavaScript had hoisted the declaration with an initial value of
undefined
.
let
's Not Do This
ES6 introduced a new keyword for variable declarations:
let
. Variables declared with the let
keyword don't participate in hoisting.
function monitorSleep() { console.log(sleepHours); // What happens here? let sleepHours = 8; }
In this case, our example code generates an error indicating
that sleepHours
has not been defined when
we reference it in the logging statement. sleepHours
cannot be referenced until the let
statement
is executed.
What If We're strict
About It?
You may be familiar with JavaScript's "strict" mode. We can enforce strict mode with the following statement:
"use strict";
We can place this statement at the top of our file to enforce strict mode throughout the file, or we can place it within a function to limit strict mode to the more narrow function scope. Strict mode requires that all variable have a formal declaration. Without strict mode, variables will implicitly be declared at the global scope.
function test() { nonstrict = 3; } test(); console.log(nonstrict);
Since strict mode is not enabled, the nonstrict
variable is treated as though it had been declared using
var
at global scope:
var nonstrict = undefined; function test() { nonstrict = 3; } test(); console.log(nonstrict);
However, it is worth noting that nonstrict
is not
available at global scope until the
test function executes at least once and tries to assign a value
to nonstrict
. It's during the assignment operation
that JavaScript recognizes the variable has not been declared
and publishes it as a global variable.
function test() { nonstrict = 3; } // No call to test(). console.log(nonstrict); // 'nonstrict' has not been defined
Conclusion
In general, it is a good practice to use let
instead of var
to avoid unexpected problems.
It is usually preferable to have JavaScript report an error
rather than unexpectedly assign undefined
to a
hoisted variable.