Javascript Hoisting and Temporal Dead Zone (TDZ)

If you read the last two articles and made it this far, this article will be a breeze, we don't really have any new concepts to cover here but if you haven't already, it's important you first read these two articles to understand the concepts covered here as they lay the foundation.

When learning javascript, there are so many terms that get thrown around and most of the time the metaphors are confusing, and adding to confusion, the information is completely wrong.

One of these confusing concepts is called hoisting.

What is Hoisting

When you hear the word hoisting the first thing that comes to mind is lifting something up.

The wrong assumption is that the javascript compiler will re-arrange the code. This simply does not happen. If you see this example out in the wild it's important to understand it's wrong.

JavaScript
console.log(x)
var x = 1

// hoisting asserts that the code is re-arranged to this
var x
console.log(x) // undefined
x = 1

This is not at all what happens. Back when we talked about Execution Context, we discussed a javascript program is processed in two phases: compilation/parsing and execution.

In the compilation phase the scopes are determined and variables are registered to their respective scopes, this creates an AST or Abstract Syntax Tree but none of the scopes are actually created until runtime.

Once we move into the execution phase, the javascript engine uses devices called execution contexts which are used to track the runtime evaluation of code. The javascript engine will look at this map of scopes and registered variables, or AST, created in the compilation phase and start initializing all the variables. Hoisting is the compile time operation of creating instructions for use at runtime detailing how variables should be initialized each time the scope is evaluated.

We can use an example to make this a bit more clear.

JavaScript
const a = 'a'
let b = 'b'
var c = 'c'

function foo() {
  console.log(a, b, c)
}

Now that we are in the execution/runtime phase, a global execution context will be created and pushed onto the top of the execution context stack or call stack. The first thing that happens before the engine starts to execute the code is the engine will come across the identifiers and initialize them. It will see in the AST created at compile time there is a global scope and the global scope has three variables: const a = 'a', let b = 'b', and var c = 'c' as well as a function declaration: function foo() { console.log (a, b, c)}. Each one of these has instructions attached from the compilation/parsing phase that let the engine know how they should be initialized.

var will be auto initialized to undefined, let and const will not be initialized, and function declarations will be auto initialized to their function reference.

The execution context will first hoist all the declarations. Think of hoisting as a runtime setup before code actually starts executing.

GlobalExecutionContext = {
  LexicalEnvironment: {
    a: uninitialized,
    b: unintialized,
    foo: <ref. to foo function object>,
    [[OuterEnv]]: null
  },
  VariableEnvironment: {
    c: undefined,
    [[OuterEnv]]: null
  }
}

Then the code will start to execute. When line 1 is reached, const a = 'a', the variable a will be initialized to the value 'a'.

Now the engine is at line 1 and initialized the variable const a = 'a'. At this exact moment in time, this is how the state of execution for our javascript engine looks, notice on line 4 b is still unintialized as our execution has not yet made it that far.

GlobalExecutionContext = {
  LexicalEnvironment: {
    a: 'a',
    b: unintialized,
    foo: <ref. to foo function object>,
    [[OuterEnv]]: null
  },
  VariableEnvironment: {
    c: undefined,
    [[OuterEnv]]: null
  }
}

Execution will continue, line 2 will be evaluated and the code block will look like this, notice b is now initialized.

GlobalExecutionContext = {
  LexicalEnvironment: {
    a: 'a',
    b: 'b',
    foo: <ref. to foo function object>,
    [[OuterEnv]]: null
  },
  VariableEnvironment: {
    c: undefined,
    [[OuterEnv]]: null
  }
}

Notice above that var c was already auto initialized to undefined, this is what hoisting is. Once line 3 is evaluated, var c will be re-assigned the value c.

GlobalExecutionContext = {
  LexicalEnvironment: {
    a: 'a',
    b: 'b',
    foo: <ref. to foo function object>,
    [[OuterEnv]]: null
  },
  VariableEnvironment: {
    c: 'c',
    [[OuterEnv]]: null
  }
}

Temporal Dead Zone (TDZ)

The temporal dead zone is not a syntactic location in code, but rather the time between a variable being created and its initialization. The time below the two highlighted lines is referred to as the temporal dead zone.

const a = 1
const b = 2

// these are already mapped and in the AST from the js compilation phase
// now in the runtime phase the variables will be created
GlobalExecutionContext = {
  LexicalEnvironment: {
    a: unintialized
    b: unintialized,
    [[OuterEnv]]: null
  }
}

// variable initialization is complete now execution begins
GlobalExecutionContext = {
  LexicalEnvironment: {
    a: 1,
    b: uninitialized,
    [[OuterEnv]]: null
  }
}

Temporal Dead Zone Error

We can see a temporal dead zone error when we try to access a variable before it's initialized.

JavaScript
// x: unintialized
console.log(x)
// here x will become initialized to 1
let x = 1
// Uncaught ReferenceError: x is not defined

Function Expressions Hoist

JavaScript
// x: undefined
console.log(x) // undefined
// here x is re assigned 1
var x = 1
console.log(x) // 1


// note that function expressions are also
// hoisted and intialized to undefined
// foo: undefined
foo() // same as undefined()
var foo = () => console.log('foo')
// Uncaught TypeError: foo is not a function

Re-declaration in the same scope

let and const declarations cannot be re-declared in the same scope, var can be re-declared.

JavaScript
let x = 1
let x = 2
// Uncaught SyntaxError: Identifier 'x' has already been declared

With var re-declaration is allowed.

JavaScript
var x = 1
var x = 2
console.log(x) // 2

Let's take a look at another example. Below when the code was being compiled, var x = 1 was scanned and a var x variable was registered to the identified scope, although it was not yet created. When the second var x is encounted on line 3, the compiler had already registered a var variable with identifier x and does nothing with it.

JavaScript
var x = 1
console.log(x) // 1
var x // this does not reset the variable
console.log(x) // still 1 not undefined

Here is another interesting example. The engine during compilation registers the identifier x from line 1 to the global scope, it then identifies a block scope and comes across the identifier x from line 3 which is then registered for the block scope. Line 3 is trying to assign a value to a variable that is still uninitialized and let and const variables can only be initialized with a full declaration, ie let x = 2.

JavaScript
let x = 1
{
  x = 3 // Uncaught ReferenceError: Cannot access 'x' before initialization
  let x = 2
}

Function Re-Declaration in the Same Scope

If functions are auto initialized to their function object, then what happens if we have two declarations for a function?

According to the spec in the section on GlobalDeclarationInstantiation

NOTE: If there are multiple function declarations for the same name, the last declaration is used.

JavaScript
function foo() {
  console.log('foo')
}

function foo() {
  console.log('bar')
}

foo() // bar

Example

Let's look at this code step by step to see how this would be executed taking hoisting into account. Copy and paste this code into the dev tools console to step through the code evaluation.

JavaScript
const a = 1
let b = 2
var c = 3

function foo() {
  const a = 1
  return a + b + c
}

foo()

foo()

During runtime, the engine will consult with the AST created in the compilation phase and start instantiating scopes and registering the respective variables for each scope, each time that scope is evaluated.

For the global execution context, in this order, the following steps occur:

  1. a global execution context will be created (ScriptEvaluation)
  2. the LexicalEnvironment and VariableEnvironment component of the global execution context will be setup
  3. the global execution context will be pushed onto the execution context stack, global execution context is now the running execution context
  4. all of the global declarations will be instantiated (GlobalDeclarationInstantiation)
    1. creates bindingings that are uninitialized for let (CreateMutableBinding) and const (CreateImmutableBinding)
    2. If there are functions to initialize, instantiates the function object (InstantiateFunctionObject)
      1. InstantiateFunctionObject adds all the methods the object will need to be a function [[Call]] ect... and returns a function object.
    3. creates a mutable binding in the object environment record (which means it is accessible on the window) and initializes to its function object created in the previous step which was the function object returned from InstantiateFunctionObject. (CreateGlobalFunctionBinding)
    4. creates mutable bindings that are intialized to undefined for var in the object environment record (CreateGlobalVarBinding). If it already exists, it's reused and assumed to be initialized.
  5. code evaluation will begin and when a declaration is evaluated, the binding will be initialized for let and const, for var the binding will be re-assigned to the respective value.
  6. the code is evaluated and the running execution context is removed from the callstack
  7. the context that is now on top of the stack is resumed

For the function execution context, in this order, the following steps occur:

  1. During runtime if a set of parethesis is encountered ( ) the operation Call() runs. This operation calls the internal [[Call]] method of the function object. The [[Call]] internal method was added to the function object in OrdinaryFunctionCreate
  2. [[Call]] is called
    1. PrepareForOrdinaryCall is called
      1. a new execution context is created
      2. a new function environment record or function scope is created with the NewFunctionEnvironment operation
      3. the LexicalEnvironment and VariableEnvironment are pointed to the function environment record that was just created
      4. running execution context is suspended
      5. this execution context is pushed on`to the execution context stack and is now the running execution context
    2. this value is bound
    3. evaluation of the function body's code begins (OrdinaryCallEvaluateBody)
      1. calls EvaluateBody which calls EvaluateFunctionBody
      2. EvaluateFunctionBody has two steps: 1. calling FunctionDeclarationInstantiation which creates the variables and hoists them and 2. Evaluation of the function, here is where the let and const variables will be initialized.
    4. this running execution context is removed from the call stack and previous execution context is restored as the running execution context.
    5. if there was a return value, then return it
  3. return the return value of [[Call]]

Taking into consideration what we just learned this is how the javascript engine view the code block from above. A global execution context will be created and our program looks like this. Imagine the below code block as the execution context stack or callstack. New execution contexts are appended to the top.

GlobalExecutionContext = {
  LexicalEnvironment: {
    a: uninitialized,
    b: unintialized,
    foo: <ref. to foo function object>,
    [[OuterEnv]]: null
  },
  VariableEnvironment: {
    c: undefined,
    [[OuterEnv]]: null
  }
}

Everything is now setup, all the declarations are hoisted, including let and const, they are just unintialized and evaluation begins. Once line 1 is evaluated the const a variable is initialized to 1.

GlobalExecutionContext = {
  LexicalEnvironment: {
    a: 1,
    b: unintialized,
    foo: <ref. to foo function object>,
    [[OuterEnv]]: null
  },
  VariableEnvironment: {
    c: undefined,
    [[OuterEnv]]: null
  }
}

line 2 is evaluated

GlobalExecutionContext = {
  LexicalEnvironment: {
    a: 1,
    b: 2,
    foo: <ref. to foo function object>,
    [[OuterEnv]]: null
  },
  VariableEnvironment: {
    c: undefined,
    [[OuterEnv]]: null
  }
}

line 3 is evaluated and var c is re-assigned to 3.

GlobalExecutionContext = {
  LexicalEnvironment: {
    a: 1,
    b: 2,
    foo: <ref. to foo function object>,
    [[OuterEnv]]: null
  },
  VariableEnvironment: {
    c: 3,
    [[OuterEnv]]: null
  }
}

line 10 is evaluated. There is nothing to do on line 5 because the function object was already created and registered in the environment record. On line 10 while the global execution context is evaluating code, the function reference foo is found with ( ) so the Call() method is called which calls the function objects internal [[Call]] method. The variables are created in FunctionDeclarationInstantiation and the execution context is ready to start evaluating the statements of the function.

FunctionExecutionContext = {
  LexicalEnvironment: {
    a: unintialized,
    [[OuterEnv]]: <ref. to GlobalExecutionContext Environment Record>
  }
}


GlobalExecutionContext = {
  LexicalEnvironment: {
    a: 1,
    b: 2,
    foo: <ref. to foo function object>,
    [[OuterEnv]]: null
  },
  VariableEnvironment: {
    c: 3,
    [[OuterEnv]]: null
  }
}

line 6 is evaluated

FunctionExecutionContext = {
  LexicalEnvironment: {
    a: 1,
    [[OuterEnv]]: <ref. to GlobalExecutionContext Environment Record>
  }
}


GlobalExecutionContext = {
  LexicalEnvironment: {
    a: 1,
    b: 2,
    foo: <ref. to foo function object>,
    [[OuterEnv]]: null
  },
  VariableEnvironment: {
    c: 3,
    [[OuterEnv]]: null
  }
}

line 7 is evaluated and a normal completion record is returned with the value 6. [[Call]] then removes the function execution context from the execution context stack, restores the global execution context as the running execution context, and returns the [[Value]] of the normal completion record which is 6. This result that is returned from [[Call]] is returned from the Call() operation.

The global execution context resumes execution and makes its way to line 12 and the process repeats.

GlobalExecutionContext = {
  LexicalEnvironment: {
    a: 1,
    b: 2,
    foo: <ref. to foo function object>,
    [[OuterEnv]]: null
  },
  VariableEnvironment: {
    c: 3,
    [[OuterEnv]]: null
  }
}

line 12 is evaluated, Call() is called invoking the function objects internal [[Call]] method, a new function execution context is created and pushed onto the top of the call stack and variables are created and hoisted.

FunctionExecutionContext = {
  LexicalEnvironment: {
    a: uninitialized,
    [[OuterEnv]]: <ref. to GlobalExecutionContext Environment Record>
  }
}


GlobalExecutionContext = {
  LexicalEnvironment: {
    a: 1,
    b: 2,
    foo: <ref. to foo function object>,
    [[OuterEnv]]: null
  },
  VariableEnvironment: {
    c: 3,
    [[OuterEnv]]: null
  }
}

line 6 is evaluated and initializes const a to 1.

FunctionExecutionContext = {
  LexicalEnvironment: {
    a: 1,
    [[OuterEnv]]: <ref. to GlobalExecutionContext Environment Record>
  }
}


GlobalExecutionContext = {
  LexicalEnvironment: {
    a: 1,
    b: 2,
    foo: <ref. to foo function object>,
    [[OuterEnv]]: null
  },
  VariableEnvironment: {
    c: 3,
    [[OuterEnv]]: null
  }
}

The rest of the steps are the same as above. When the function execution context is popped from the callstack and the global execution context resumes evaluation, once the global execution context finishes, it will also be popped and the callstack will be empty.

Notice how when foo() is invoked a second time on line 12, const a is created again new, it is again set to uninitialized, and then it is initialized during evaluation when the engine reaches the declaration const a = 1. This is why we said hoisting is the auto initialization of variables each time that scope is entered.

Review

Hoisting does not re-arrange any code, it simply describes how the js engine auto initializes variables during runtime. let and const do hoist to the top of their scopes and are set to uninitialized until execution begins and the line where they are declared is evaluated, this time window is the temporal dead zone.