JavaScript is an interesting language, to say the least, and one of its features that seems to confuse people the most is variable “hoisting”. In this blag post I hope to clarify what “hosting” is, and why it exists.
In the beginning, JS only had one kind of variable, a var
. var
declarations are what we call function scoped – that is, they are
hoisted to the top of the function that encloses them. If they aren’t in
a function, they are hoisted to the top of the script that encloses them.
(function() {
var hello = 'world';
console.log('hello', hello);
{
var myFunc = function() { return 42; };
}
var foo = 'bar';
function myOtherFunc() { return 6 * 7; }
}());
Will be interpreted by JS as:
(function() {
function myOtherFunc() { return 6 * 7; }
var hello, myFunc, foo;
hello = 'world';
console.log('hello', hello);
{
myFunc = function() { return 42; };
}
foo = 'bar';
}());
Once you understand this, it’s fairly simple to see why you don’t
get an exception when you access a var
before it is declared,
or why you can call functions before you define them.
But let’s get more complex. In 2015, we got the twins let
and const
.
These are not function scoped, but rather, lexically scoped. “Lexically”
here basically means “statically”. In a function scope, variable are
hoisted to the top of the dynamic boundary of the enclosing function.
In a lexical scope, variables are hoisted to the top of the enclosing block.
But that’s not the end of the story! let
and const
, unlike var
, will
throw if you try to access them before you define them:
console.log(myVar); // logs `undefined`
var myVar = 5;
console.log(myLet); // throws "ReferenceError: myLet is not defined"
let myLet = 5;
Now this may be initially confusing, as I said before that they are hoisted
like var
, and that is still true, but the hoisting is slightly different.
let
and const
, unlike var
, are not just undeclared or declared. They are
either undeclared, declared, or initialized. Lets take a look at what this
means:
{
console.log(myLet); // ReferenceError
let myLet = 5;
}
Becomes this:
{
declare myLet;
console.log(myLet); // ReferenceError
initialize myLet = 5;
}
When the JS engine tries to access these lexical declarations before they are initialized, it throws a ReferenceError saying they aren’t defined. This behaviour is called a temporal dead zone, or TDZ. Temporal means time, and dead zone means… dead zone. Basically, the temporal dead zone is the time span in which the variable is declared but not initialized.
So now you’re probably wondering, “why make this big complex TDZ thing?”, and that’s a good question. The answer has to do with lexical scoping:
const myConst = 5;
console.log(myConst);
{
const myConst = 10;
console.log(myConst);
}
When we run this code, it will produce 5, 10
. New lexical scopes can override
the lexical declarations of outer scopes. But what if we do this:
const myConst = 5;
console.log(myConst);
{
console.log(myConst); // o.O
const myConst = 10;
console.log(myConst);
}
Without hoisting, this would log 5, 5, 10
. Now, you may think that would be
reasonable behaviour, and some other languages even allow this. But it was
decided that this would be confusing in the context of JS, and so with hoisting,
it becomes this:
declare myConst;
initialize myConst = 5;
console.log(myConst);
{
declare myConst; // beginning of TDZ for `myConst`
console.log(myConst); // TDZ access of `myConst` throws a ReferenceError
initialize myConst = 10; // `myConst` is initialized, and TDZ ends
console.log(myConst);
}
As you can see, the inner declare myConst
stops our added console line from accessing
the outer myConst
, and makes it throw a ReferenceError.
JS has three kinds of variables: var
, let
, and const
. They are all hoisted too, but
in different ways. var
is set to undefined
when it is hoisted, but let
and const
aren’t valid until they are initially assigned a value. The time between the hoisted declaration
and being initially assigned a value is known as the temporal dead zone, or TDZ.