ref – http://adripofjavascript.com/blog/drips/variable-and-function-hoisting.html
http://jsrocks.org/2015/01/temporal-dead-zone-tdz-demystified
Hoisting is about a variable or function definition (that’s being used or initialized) being available beforehand.
For example, if we declare and initialize a variable to a string, that variable is actually available beforehand at the very top. This is because the interpreter will get all the variables, and declare them at the very top first.
1 2 3 4 5 6 7 8 |
// declaredLater "hoisted" here // var declaredLater; console.log(declaredLater); // undefined var declaredLater = "Now it's defined!"; // even though the variable is declared and initialized here, its actually beforehand. console.log(declaredLater); // "Now it's defined!" |
global, local scope variable over-rides
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
var name = "Baggins"; (function () { // var name of THIS LOCAL SCOPE hoisted here // var name; // Outputs: "Original name was undefined" // That's why name here is undefined. The name here // accesses the local scope's name. console.log("Original name was " + name); var name = "Underhill"; // Outputs: "New name is Underhill" console.log("New name is " + name); })(); |
Function definition declaration
Function definition declarations are hoisted.
1 2 3 4 5 |
isItHoisted(); function isItHoisted() { console.log("Yes!"); } |
function definition hoisting only occurs for function declarations. In other words,
when you declare the function definition, then you can simply call it.
However, when the function definition is part of a function expression, then hoisting will not work.
For example, when we have a function expression variable that takes on the function definition, we cannot hoist the function expression variable.
1 2 3 4 5 6 7 8 9 10 |
functionDefinition(); //functionDeclaration(); // ERROR! function functionDefinition() { console.log("Definition hoisted!"); } var functionDeclaration = function () { console.log("Definition not hoisted!"); }; |
Function definitions as part of function expression are not hoisted
If you have a function definition, and its part of a function expression, then you CANNOT do hoisting:
1 2 3 4 5 6 7 8 9 10 |
// ReferenceError: funcName is not defined //funcName(); // TypeError: undefined is not a function //varName(); // won't get hoisted if its part of a function expression var varName = function funcName() { console.log("HADOOOOO KEN!"); }; |
Class declarations ARE NOT hoisted
class declarations are not hoisted like function declarations.
For example, if you place the following code above the Animal class declaration section, you will get a ReferenceError:
1 2 3 4 5 6 7 8 9 10 11 |
let dog = new Animal('Dog'); // Uncaught ReferenceError: Animal is not defined class Animal { constructor(type) { this.type = type; } identify() { console.log(type); } } |
Hoisting
var is function scoped. It is available in the whole function even before being declared
Declarations are hoisted. So you can use a variable before it has been declared.
Initializations are NOT hoisted. If you are using var, ALWAYS declare your variables at the top.
We declare x globally.
There is an if block, and in it, a var is declared with the same name.
that name gets hoisted to the top of the function. The name is the same
as the global one (x), and thus over-rides it.
es5
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
var x = 'outer' function test(inner) { // var x; // undefined if (inner) { var x = 'inner' // x here gets hoisted to the top of the function body. // It overrides the global x. // test now has its own copy of x. return x; } return x; } console.log(test(false)); // undefined console.log(test(true)); // inner |
translates to:
1 2 3 4 5 6 7 8 9 10 |
var x = 'outer'; function test(inner) { var x; // hoisted declaration if (inner) { x = 'inner' // initialization NOT HOISTED return x; } return x; } |
In es6, when we use let, it works as expected:
1 2 3 4 5 6 7 8 9 10 11 12 |
let x = 'outer' function test(inner) { if(inner) { let x = 'inner' return x; } return x; } test(false); // outer test(true); // inner |
let will hoist the variable to the top of the BLOCK (NOT top function like var)
Referencing the variable in the block before the variable declaration results in a ReferenceError
Temporal Dead Zone
“Temporal Dead Zone” is the zone from the start of the block until the variable is declared
1 2 3 4 5 6 7 |
let x = 'outer scope'; (function() { // block // start of temporary dead zone console.log(x); // end of temporary dead zone let x = 'inner scope'; // variable is declared to x, then initialized to a string. }()); |
Can you guess what console.log(x) will print now? Well, actually, the answer is nothing — the code above will throw a ReferenceError due to the TDZ semantics. That is because let/const declarations do hoist, but they throw errors when accessed before being initialized (instead of returning undefined as var would).
let and const declarations define variables that are scoped to the running execution context’s LexicalEnvironment.
In other words, the variables are created when their containing Lexical Environment is instantiated. This means whenever control flow enters a new scope (e.g. module, function or block scope), all the let/const bindings belonging to the given scope are instantiated before any code inside of the given scope is executed — in other words,
let/const declarations DO HOIST
…but may not be accessed in any way until the variable’s LexicalBinding is evaluated.
This is the Temporary Dead Zone
A given let/const-declared binding can’t be accessed in any way (read/write) until control flow has evaluated the declaration statement — that does not refer to the hoisting, but rather to where the declaration actually is in the code.
1 2 3 4 5 6 7 |
// Accessing `x` here before control flow evaluates the `let x` statement // would throw a ReferenceError due to TDZ. // console.log(x); let x = 42; // From here on, accessing `x` is perfectly fine! console.log(x); |
And the last part:
If a LexicalBinding in a let declaration does not have an Initializer the variable is assigned the value undefined when the LexicalBinding is evaluated.
This simply means that:
1 |
let x; |
Is equivalent to:
1 |
let x = undefined; |
Trying to access x in any way before control flow evaluates the initializer (or the “implicit” = undefined initializer) will result in a ReferenceError, while accessing it after the control flow has evaluated the declaration will work fine. i.e reading the x variable after the let x declaration in both samples above would return undefined.
1 |
let x = x; |
Does the code execute without errors? What is the value of x after the code executes?
First off, remember that a let/const variable only counts as initialized after its initializer has been fully evaluated — that is, after the assignment’s right-hand side expression has been evaluated and its resulting value has been assigned to the declared variable.
1 |
let x = x; // 1) we evaluate right hand first, which is x |
In this case, the right-hand side expression tries to read the x variable, but x’s initializer has not been fully evaluated yet.
1 2 |
// 2) x here is hoisted..and is in TDZ. let x = x; // 1) we evaluate right hand first, which is x |
Thus, trying to read x at 2) results in a TDZ, and thus will throw a ReferenceError.
— in fact we are evaluating it at that point — so x still counts as uninitialized at that point and thus trying to read it throws a TDZ ReferenceError.
Other Examples
1 2 3 4 5 6 |
let a = f(); const b = 2; function f() { return b; } |
In the first line, the f() call makes control flow jump to and execute the f function, which in turn tries to read the b constant which, at this point in the runtime, is still uninitialized (in TDZ) and thus throws a ReferenceError. As you can see, TDZ semantics apply when trying to access variables from parent scopes as well.
Here, we have parameters a and b defined in the IIFE. Parameters are evaluated from left to right. At the left, we have a = 1.
Then the next parameter b takes on a, which at that time, evaluates to 1, thus b = 1.
1 2 3 4 5 |
// Works fine. (function(a, b = a) { console.log(a) // 1 console.log(b) // 1 }(1, undefined)); |
The a, b are parameters. They are bound to the IIFE. thus, parameter b’s scope is in the IIFE. b gets hoisted. When a tries to assign b, it will throw a ReferenceError. Because b does not have a value yet.
1 2 3 4 5 |
let b = 1; // global b (function(a = b, b) { console.log(a, b); }(undefined, 2)); |
more examples…
First we implement Numbers class with a simple constructor where we initialize
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
// hoisting. numbersObject available here class Numbers { constructor(array) { console.log(`constructring Numbers object`) this.array = array; } addNumber(number) { console.log(`--> addNumber ${number}`); // numbersObject available globally at line 1. It was undefined // but before this function was called, it was initialized // to a new Numbers object at * console.log(numbersObject); if (number !== undefined) { console.log(`number is valid!`) this.array.push(number); return; } // this points to the object referred to by addNumber. console.log(`<-- returning another function from addNumber`) return (number) => { console.log(`trying to push ${number}`) if (this === numbersObject) { console.log(`this object refers to our instance`) this.array.push(number); } }; } } // * var numbersObject = new Numbers([]); numbersObject.addNumber(1); var addMethod = numbersObject.addNumber(); addMethod(5); console.log(numbersObject.array); // => [1, 5] |