JavaScript Hoisting and Temporal Dead Zone (TDZ)

Hoisting is an important concept to understand in JavaScript but it is very misunderstood. Many online resources and tutorials teach that the code is "hoisted" or "lifted up" to the top of the scope. This article will attempt to show why this is not the case at all.

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 rearrange 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 rearranged 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 and execution.

In the compilation phase the scopes are determined and variables are registered to their respective scopes, this creates an Abstract Syntax Tree or AST, 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 simply refers to the process in which the interpreter seems to move the declaration of variables, functions, classes ect. to the top of their scope prior to code execution. However, to be a bit more technical, we could say that 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: GlobalEnvironmentRecord,
  VariableEnvironment: GlobalEnvironmentRecord
}

GlobalEnvironmentRecord = {
  Type: GlobalEnvironmentRecord,
  [[ObjectRecord]]: {
    Type: ObjectEnvironmentRecord,
    [[BindingObject]]: <global object>,
    Bindings: {
      // Built-in globals (e.g., Math, Array, JSON, etc.)
      c: undefined
    },
    [[OuterEnv]]: null
  },
  [[DeclarativeRecord]]: {
    Type: DeclarativeEnvironmentRecord,
    Bindings: {
      a: uninitialized,
      b: unintialized,
      foo: <ref. to foo function object>
    },
    [[OuterEnv]]: null
  },
  [[GlobalThisValue]]: <global object>,
  [[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: GlobalEnvironmentRecord,
  VariableEnvironment: GlobalEnvironmentRecord
}

GlobalEnvironmentRecord = {
  Type: GlobalEnvironmentRecord,
  [[ObjectRecord]]: {
    Type: ObjectEnvironmentRecord,
    [[BindingObject]]: <global object>,
    Bindings: {
      // Built-in globals (e.g., Math, Array, JSON, etc.)
      c: undefined
    },
    [[OuterEnv]]: null
  },
  [[DeclarativeRecord]]: {
    Type: DeclarativeEnvironmentRecord,
    Bindings: {
      a: 'a',
      b: unintialized,
      foo: <ref. to foo function object>
    },
    [[OuterEnv]]: null
  },
  [[GlobalThisValue]]: <global object>,
  [[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: GlobalEnvironmentRecord,
  VariableEnvironment: GlobalEnvironmentRecord
}

GlobalEnvironmentRecord = {
  Type: GlobalEnvironmentRecord,
  [[ObjectRecord]]: {
    Type: ObjectEnvironmentRecord,
    [[BindingObject]]: <global object>,
    Bindings: {
      // Built-in globals (e.g., Math, Array, JSON, etc.)
      c: undefined
    },
    [[OuterEnv]]: null
  },
  [[DeclarativeRecord]]: {
    Type: DeclarativeEnvironmentRecord,
    Bindings: {
      a: 'a',
      b: 'b',
      foo: <ref. to foo function object>
    },
    [[OuterEnv]]: null
  },
  [[GlobalThisValue]]: <global object>,
  [[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 reassigned the value c.

GlobalExecutionContext = {
  LexicalEnvironment: GlobalEnvironmentRecord,
  VariableEnvironment: GlobalEnvironmentRecord
}

GlobalEnvironmentRecord = {
  Type: GlobalEnvironmentRecord,
  [[ObjectRecord]]: {
    Type: ObjectEnvironmentRecord,
    [[BindingObject]]: <global object>,
    Bindings: {
      // Built-in globals (e.g., Math, Array, JSON, etc.)
      c: 'c'
    },
    [[OuterEnv]]: null
  },
  [[DeclarativeRecord]]: {
    Type: DeclarativeEnvironmentRecord,
    Bindings: {
      a: 'a',
      b: 'b',
      foo: <ref. to foo function object>
    },
    [[OuterEnv]]: null
  },
  [[GlobalThisValue]]: <global object>,
  [[OuterEnv]]: null
}
All variables are hoisted, the difference is let and const are not auto initialized, they remain uninitialized, var is auto initialized to undefined.

Temporal Dead Zone (TDZ)

The temporal dead zone (TDZ) is the period of time between entering a scope and the point where a let or const variable is declared and initialized to a value. During this time, the variable exists in the environment record but its in an uninitialized state. Accessing it before initialization results in a ReferenceError. If no initial value is provided, the variable is initialized to undefined when the declaration is reached.

The term "temporal" is used to refer to the fact that this zone depends on the order of execution.

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: GlobalEnvironmentRecord,
  VariableEnvironment: GlobalEnvironmentRecord
}

GlobalEnvironmentRecord = {
  Type: GlobalEnvironmentRecord,
  [[ObjectRecord]]: {
    Type: ObjectEnvironmentRecord,
    [[BindingObject]]: <global object>,
    Bindings: {
      // Built-in globals (e.g., Math, Array, JSON, etc.)
    },
    [[OuterEnv]]: null
  },
  [[DeclarativeRecord]]: {
    Type: DeclarativeEnvironmentRecord,
    Bindings: {
      a: unintialized,
      b: unintialized
    },
    [[OuterEnv]]: null
  },
  [[GlobalThisValue]]: <global object>,
  [[OuterEnv]]: null
}

// variable initialization is complete now execution begins
GlobalExecutionContext = {
  LexicalEnvironment: GlobalEnvironmentRecord,
  VariableEnvironment: GlobalEnvironmentRecord
}

GlobalEnvironmentRecord = {
  Type: GlobalEnvironmentRecord,
  [[ObjectRecord]]: {
    Type: ObjectEnvironmentRecord,
    [[BindingObject]]: <global object>,
    Bindings: {
      // Built-in globals (e.g., Math, Array, JSON, etc.)
    },
    [[OuterEnv]]: null
  },
  [[DeclarativeRecord]]: {
    Type: DeclarativeEnvironmentRecord,
    Bindings: {
      a: 1,
      b: unintialized
    },
    [[OuterEnv]]: null
  },
  [[GlobalThisValue]]: <global object>,
  [[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

A let declaration within a function's body cannot have the same name as a paramete

JavaScript
function foo(x) {
  let x = 1; // SyntaxError: Identifier 'x' has already been declared
}
ReferenceError error occurs when trying to access a variable that doesn't exist.

Here is another example of a temporal dead zone error. The if block is evaluated because the outer const x has a value. However, due to lexical scoping this value is not available inside the block. The identifier inside the block is a different const x. The expression x + 1 throws a ReferenceError because initialization of const x inside the block has not completed yet - it is still in the temporal dead zone.

JavaScript
function foo() {
  const x = 5

  if (x) {
    const x = x + 1 // ReferenceError
    //        ^ does not refer to outer const x = 5
  }
}

foo()

Here is another interesting and perhaps hard to spot temporal dead zone error. The code below fails because there is a new block scope created for the for...of loop's block. The identifier x.z is resolved to the property z of let x which is uninitialized as its still in the temporal dead zone since its declaration has not yet been reached.

JavaScript
function foo(x) {
  // x is defined here
  console.log(x); // { z: [1, 2, 3] }

  for (let x of x.z) {
    //     ^    ^ ReferenceError
    console.log(x)
  }
}

foo({ z: [1, 2, 3] })

Function expressions hoist

Function declarations are initialized to their function reference so this code will work as expected.

JavaScript
foo()

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

The below code will not work because the function expression is hoisted and initialized to undefined and calling foo() in this example is the same as calling undefined().

JavaScript
foo()
var foo = () => console.log('foo')
// Uncaught TypeError: foo is not a function

Redeclaration in the same scope

let and const declarations cannot be redeclared in the same scope, var can be redeclared.

JavaScript
let x = 1
let x = 2
// Uncaught SyntaxError: Identifier 'x' has already been declared
SyntaxError means there's s a syntactic error and prevents the code from executing at all.

With var redeclaration is allowed.

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

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 encountered, the compiler had already registered a var variable with identifier x and does nothing with it.

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
JavaScript
var x = 1
console.log(x) // 1
var x // this does not reset the variable
console.log(x) // still 1 not undefined

Hoisting and block scope

In the below code block, there are two scopes: a global scope and a block scope. In the global scope there is an identifier x registered, and in the block scope there is also an identifier x which is registered.

At the point in time on the third line during code execution, the identifier x in the global scope has been initialized through declaration with a value of 1, and the identifier x in the block scope is uninitialized. In order to initialize the x identifier for the block scope, it has to be declared and intialized with a value. Attempting to assign a value to the block scoped x before it has been initialized results in a ReferenceError.

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

Variables declared with var are not block scoped.

JavaScript
var x = 1
{
  var x = 3
}

console.log(x) // 3

Compare this with let or const:

JavaScript
const x = 1
{
  const x = 3
}

console.log(x) // 1

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 reassigned 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: GlobalEnvironmentRecord,
  VariableEnvironment: GlobalEnvironmentRecord
}

GlobalEnvironmentRecord = {
  Type: GlobalEnvironmentRecord,
  [[ObjectRecord]]: {
    Type: ObjectEnvironmentRecord,
    [[BindingObject]]: <global object>,
    Bindings: {
      // Built-in globals (e.g., Math, Array, JSON, etc.)
      c: undefined
    },
    [[OuterEnv]]: null
  },
  [[DeclarativeRecord]]: {
    Type: DeclarativeEnvironmentRecord,
    Bindings: {
      a: uninitialized,
      b: unintialized,
      foo: <ref. to foo function object>
    },
    [[OuterEnv]]: null
  },
  [[GlobalThisValue]]: <global object>,
  [[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: GlobalEnvironmentRecord,
  VariableEnvironment: GlobalEnvironmentRecord
}

GlobalEnvironmentRecord = {
  Type: GlobalEnvironmentRecord,
  [[ObjectRecord]]: {
    Type: ObjectEnvironmentRecord,
    [[BindingObject]]: <global object>,
    Bindings: {
      // Built-in globals (e.g., Math, Array, JSON, etc.)
      c: undefined
    },
    [[OuterEnv]]: null
  },
  [[DeclarativeRecord]]: {
    Type: DeclarativeEnvironmentRecord,
    Bindings: {
      a: 1,
      b: unintialized,
      foo: <ref. to foo function object>
    },
    [[OuterEnv]]: null
  },
  [[GlobalThisValue]]: <global object>,
  [[OuterEnv]]: null
}

line 2 is evaluated

GlobalExecutionContext = {
  LexicalEnvironment: GlobalEnvironmentRecord,
  VariableEnvironment: GlobalEnvironmentRecord
}

GlobalEnvironmentRecord = {
  Type: GlobalEnvironmentRecord,
  [[ObjectRecord]]: {
    Type: ObjectEnvironmentRecord,
    [[BindingObject]]: <global object>,
    Bindings: {
      // Built-in globals (e.g., Math, Array, JSON, etc.)
      c: undefined
    },
    [[OuterEnv]]: null
  },
  [[DeclarativeRecord]]: {
    Type: DeclarativeEnvironmentRecord,
    Bindings: {
      a: 1,
      b: 2,
      foo: <ref. to foo function object>
    },
    [[OuterEnv]]: null
  },
  [[GlobalThisValue]]: <global object>,
  [[OuterEnv]]: null
}

line 3 is evaluated and var c is reassigned to 3.

GlobalExecutionContext = {
  LexicalEnvironment: GlobalEnvironmentRecord,
  VariableEnvironment: GlobalEnvironmentRecord
}

GlobalEnvironmentRecord = {
  Type: GlobalEnvironmentRecord,
  [[ObjectRecord]]: {
    Type: ObjectEnvironmentRecord,
    [[BindingObject]]: <global object>,
    Bindings: {
      // Built-in globals (e.g., Math, Array, JSON, etc.)
      c: 3
    },
    [[OuterEnv]]: null
  },
  [[DeclarativeRecord]]: {
    Type: DeclarativeEnvironmentRecord,
    Bindings: {
      a: 1,
      b: 2,
      foo: <ref. to foo function object>
    },
    [[OuterEnv]]: null
  },
  [[GlobalThisValue]]: <global object>,
  [[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.

FooExecutionContext = {
  LexicalEnvironment: FooEnvironmentRecord,
  VariableEnvironment: FooEnvironmentRecord
}

FooEnvironmentRecord = {
  type: FunctionEnvironmentRecord,
  Bindings: {
    a: uninitialized
  },
  [[ThisValue]]: window, // for browsers
  [[ThisBindingStatus]]: "initialized",
  [[FunctionObject]]: <reference to foo>,
  [[NewTarget]]: undefined,
  [[OuterEnv]]: GlobalEnvironmentRecord
}

GlobalExecutionContext = {
  LexicalEnvironment: GlobalEnvironmentRecord,
  VariableEnvironment: GlobalEnvironmentRecord
}

GlobalEnvironmentRecord = {
  Type: GlobalEnvironmentRecord,
  [[ObjectRecord]]: {
    Type: ObjectEnvironmentRecord,
    [[BindingObject]]: <global object>,
    Bindings: {
      // Built-in globals (e.g., Math, Array, JSON, etc.)
      c: 3
    },
    [[OuterEnv]]: null
  },
  [[DeclarativeRecord]]: {
    Type: DeclarativeEnvironmentRecord,
    Bindings: {
      a: 1,
      b: 2,
      foo: <ref. to foo function object>
    },
    [[OuterEnv]]: null
  },
  [[GlobalThisValue]]: <global object>,
  [[OuterEnv]]: null
}

line 6 is evaluated

FooExecutionContext = {
  LexicalEnvironment: FooEnvironmentRecord,
  VariableEnvironment: FooEnvironmentRecord
}

FooEnvironmentRecord = {
  type: FunctionEnvironmentRecord,
  Bindings: {
    a: 1
  },
  [[ThisValue]]: window, // for browsers
  [[ThisBindingStatus]]: "initialized",
  [[FunctionObject]]: <reference to foo>,
  [[NewTarget]]: undefined,
  [[OuterEnv]]: GlobalEnvironmentRecord
}

GlobalExecutionContext = {
  LexicalEnvironment: GlobalEnvironmentRecord,
  VariableEnvironment: GlobalEnvironmentRecord
}

GlobalEnvironmentRecord = {
  Type: GlobalEnvironmentRecord,
  [[ObjectRecord]]: {
    Type: ObjectEnvironmentRecord,
    [[BindingObject]]: <global object>,
    Bindings: {
      // Built-in globals (e.g., Math, Array, JSON, etc.)
      c: 3
    },
    [[OuterEnv]]: null
  },
  [[DeclarativeRecord]]: {
    Type: DeclarativeEnvironmentRecord,
    Bindings: {
      a: 1,
      b: 2,
      foo: <ref. to foo function object>
    },
    [[OuterEnv]]: null
  },
  [[GlobalThisValue]]: <global object>,
  [[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: GlobalEnvironmentRecord,
  VariableEnvironment: GlobalEnvironmentRecord
}

GlobalEnvironmentRecord = {
  Type: GlobalEnvironmentRecord,
  [[ObjectRecord]]: {
    Type: ObjectEnvironmentRecord,
    [[BindingObject]]: <global object>,
    Bindings: {
      // Built-in globals (e.g., Math, Array, JSON, etc.)
      c: 3
    },
    [[OuterEnv]]: null
  },
  [[DeclarativeRecord]]: {
    Type: DeclarativeEnvironmentRecord,
    Bindings: {
      a: 1,
      b: 2,
      foo: <ref. to foo function object>
    },
    [[OuterEnv]]: null
  },
  [[GlobalThisValue]]: <global object>,
  [[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.

FooExecutionContext = {
  LexicalEnvironment: FooEnvironmentRecord,
  VariableEnvironment: FooEnvironmentRecord
}

FooEnvironmentRecord = {
  type: FunctionEnvironmentRecord,
  Bindings: {
    a: uninitialized
  },
  [[ThisValue]]: window, // for browsers
  [[ThisBindingStatus]]: "initialized",
  [[FunctionObject]]: <reference to foo>,
  [[NewTarget]]: undefined,
  [[OuterEnv]]: GlobalEnvironmentRecord
}

GlobalExecutionContext = {
  LexicalEnvironment: GlobalEnvironmentRecord,
  VariableEnvironment: GlobalEnvironmentRecord
}

GlobalEnvironmentRecord = {
  Type: GlobalEnvironmentRecord,
  [[ObjectRecord]]: {
    Type: ObjectEnvironmentRecord,
    [[BindingObject]]: <global object>,
    Bindings: {
      // Built-in globals (e.g., Math, Array, JSON, etc.)
      c: 3
    },
    [[OuterEnv]]: null
  },
  [[DeclarativeRecord]]: {
    Type: DeclarativeEnvironmentRecord,
    Bindings: {
      a: 1,
      b: 2,
      foo: <ref. to foo function object>
    },
    [[OuterEnv]]: null
  },
  [[GlobalThisValue]]: <global object>,
  [[OuterEnv]]: null
}

line 6 is evaluated and initializes const a to 1.

FooExecutionContext = {
  LexicalEnvironment: FooEnvironmentRecord,
  VariableEnvironment: FooEnvironmentRecord
}

FooEnvironmentRecord = {
  type: FunctionEnvironmentRecord,
  Bindings: {
    a: 1
  },
  [[ThisValue]]: window, // for browsers
  [[ThisBindingStatus]]: "initialized",
  [[FunctionObject]]: <reference to foo>,
  [[NewTarget]]: undefined,
  [[OuterEnv]]: GlobalEnvironmentRecord
}

GlobalExecutionContext = {
  LexicalEnvironment: GlobalEnvironmentRecord,
  VariableEnvironment: GlobalEnvironmentRecord
}

GlobalEnvironmentRecord = {
  Type: GlobalEnvironmentRecord,
  [[ObjectRecord]]: {
    Type: ObjectEnvironmentRecord,
    [[BindingObject]]: <global object>,
    Bindings: {
      // Built-in globals (e.g., Math, Array, JSON, etc.)
      c: 3
    },
    [[OuterEnv]]: null
  },
  [[DeclarativeRecord]]: {
    Type: DeclarativeEnvironmentRecord,
    Bindings: {
      a: 1,
      b: 2,
      foo: <ref. to foo function object>
    },
    [[OuterEnv]]: null
  },
  [[GlobalThisValue]]: <global object>,
  [[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 rearrange any code, it simply describes how variables are initialized during runtime. let and const are set to uninitialized until code execution reaches the place where the variable is declared and initialized to a value. If no initial value was specified with the variable declaration, it will be initialized with a value of undefined. For var declarations, they will be set to undefined before they are declared.

The period of time between entering a scope and the point where a let or const variable is declared and initialized to a value is referred to as the temporal dead zone. Accessing let or const before initialization results in a ReferenceError.