The Compiler Strikes Again
To answer this question, we need to refer back to Chapter 1, and our discussion of compilers. Recall that the Engine actually will compile your JavaScript code before it interprets it. Part of the compilation phase was to find and associate all declarations with their appropriate scopes. Chapter 2 showed us that this is the heart of Lexical Scope.
So, the best way to think about things is that all declarations, both variables and functions, are processed first, before any part of your code is executed.
When you see var a = 2;
, you probably think of that as one statement. But JavaScript actually thinks of it as two statements: var a;
and a = 2;
. The first statement, the declaration, is processed during the compilation phase. The second statement, the assignment, is left in place for the execution phase.
Our first snippet then should be thought of as being handled like this:
var a;
a = 2;
console.log( a );
…where the first part is the compilation and the second part is the execution.
Similarly, our second snippet is actually processed as:
var a;
console.log( a );
a = 2;
So, one way of thinking, sort of metaphorically, about this process, is that variable and function declarations are “moved” from where they appear in the flow of the code to the top of the code. This gives rise to the name “Hoisting”.
In other words, the egg (declaration) comes before the chicken (assignment).
Note: Only the declarations themselves are hoisted, while any assignments or other executable logic are left in place. If hoisting were to re-arrange the executable logic of our code, that could wreak havoc.
foo();
function foo() {
console.log( a ); // undefined
var a = 2;
}
The function foo
‘s declaration (which in this case includes the implied value of it as an actual function) is hoisted, such that the call on the first line is able to execute.
It’s also important to note that hoisting is per-scope. So while our previous snippets were simplified in that they only included global scope, the foo(..)
function we are now examining itself exhibits that var a
is hoisted to the top of foo(..)
(not, obviously, to the top of the program). So the program can perhaps be more accurately interpreted like this:
function foo() {
var a;
console.log( a ); // undefined
a = 2;
}
foo();
Function declarations are hoisted, as we just saw. But function expressions are not.
foo(); // not ReferenceError, but TypeError!
var foo = function bar() {
// ...
};
The variable identifier foo
is hoisted and attached to the enclosing scope (global) of this program, so foo()
doesn’t fail as a ReferenceError
. But foo
has no value yet (as it would if it had been a true function declaration instead of expression). So, foo()
is attempting to invoke the undefined
value, which is a TypeError
illegal operation.
Also recall that even though it’s a named function expression, the name identifier is not available in the enclosing scope:
foo(); // TypeError
bar(); // ReferenceError
var foo = function bar() {
// ...
};
This snippet is more accurately interpreted (with hoisting) as:
var foo;
foo(); // TypeError
bar(); // ReferenceError
foo = function() {
var bar = ...self...
// ...
}