JavaScript Execution Context

An execution context is what the JavaScript engine uses to track the runtime evaluation of code.

Having a solid understanding of execution context in JavaScript is essential in order to understand other fundamental concepts developers often find confusing such as scope, closures and hoisting. Although execution context is not a feature you can access directly within your code, it is a concept defined by the ECMAScript specification detailing how the engine evaluates and executes your programs.

How the JavaScript engine executes code

The first thing to understand is the JavaScript engine executes code in two phases:

  1. Compilation phase
  2. Execution phase
Although ECMAScript does not define a concrete compilation phase, it specifies static semantics and early error steps that occur before runtime evaluation. In practice, engines perform parsing source code and scope analysis prior to executing it.

Compilation Phase

Compilation consists of a few different phases. Before our code can be executed it needs to be broken up into tokens, this is typically referred to as tokenizing/lexing. The parser takes in this array of tokens and creates an abstract syntax tree or AST which will later be consumed during code generation, which is the process of transforming the AST into executable code.

The following example demonstrates JavaScript performs a separate compilation phase before execution. Because a SyntaxError is detected during compilation, the code never reaches the execution phase, which is why console.log(a) never runs.

let a = 'a'

console.log(a) // this never runs

a = .'a' // Uncaught SyntaxError: Unexpected token '.'

Notice how 'a' is never logged to the console. The only way the JavaScript engine could know about this error is if the entire program was parsed first. If it was ran from the top down, the console.log(a) would run, then an error would be thrown.

The same thing happens below even though the function is never invoked.

const a = 'a'
console.log(a)

function foo() {
  let b = 'b'
  console.log(b)
  b = .'b' // Uncaught SyntaxError: Unexpected token '.'
}

Here is another interesting example, let is declared twice in the same scope. Even though the function is never invoked a SyntaxError is thrown.

function foo() {
  let a = 1
  let a = 2
}
// Uncaught SyntaxError: Identifier 'a' has already been declared

Lexical Analysis (Lexing/Scanning)

The first step in processing JavaScript code is lexical analysis. This involves reading the raw source code character by character and grouping them into meaningful units called tokens. These tokens represent the basic building blocks of the language, such as if, while, const, let, +, =, {}, () ect.

The ECMAScript specification, which defines JavaScript, includes a detailed "Lexical Grammar" section. This grammar precisely defines how characters are grouped into tokens, including rules for identifiers, keywords, literals, and how whitespace and line terminators are handled.

The lexer is what implements the lexical grammar.

We can see below in the code block there is a simple variable declaration, but in order for this program to be executed it first needs to be tokenized.

const x = 1

This code will be broken up into tokens const, x, =, and 1.

Syntax Analysis (Parsing)

The JavaScript engine feeds the token stream to the parser, which constructs the AST - a hierarchical representation of the program’s syntactic structure. During this step, the parser enforces JavaScript’s syntactic grammar. If a violation is found, compilation stops with a SyntaxError.

The actual resolution of scopes and identifier bindings is performed later during the semantic analysis phase.

This is where the term lexical scope comes from. Lexical scope is defined by the physical location of where functions and variables are declared in the code.

Semantic Analysis

During semantic analysis, the engine traverses the AST to construct and consult a separate scope structure representing lexical environments.

This scope structure is used to create bindings, resolve identifier references to the correct bindings (including shadowing), and detect early errors such as invalid or duplicate declarations within the same scope.

At this stage, scopes and bindings exist only as part of the program’s static structure. Memory for variables is allocated later during execution, when the runtime creates environment records for each scope according to the structure determined during semantic analysis.

Scopes are identified during the semantic analysis phase of compilation and instantiated as environment records at runtime.

In the code below, the analyzer associates bindings with their correct lexical scopes. const x = 1 belongs to the function scope, while const x = 2 belongs to the inner block scope created by the if statement. If two const x declarations appeared in the same block, the analyzer would report a SyntaxError during static semantic analysis.

function foo() {
  const x = 1

  if (x > 0) {
    const x = 2
    console.log(x)
  }
}

Execution Phase

Once the JavaScript engine has parsed the source code and generated the executable code during the compilation phase, it proceeds to the execution phase, where that code is evaluated within execution contexts.

Execution Context

An execution context is created whenever a function is invoked or code is evaluated in global scope.

Execution contexts are tracked using the execution context stack, commonly called the callstack. Each entry in the callstack represents an execution context, and the topmost entry is referred to by the specification as the running execution context. At any point during execution, the running execution context may be suspended and a different execution context will become the running execution context.

After the execution context is created and pushed onto the execution context stack, the engine performs declaration instantiation, which creates and initializes bindings before any statements execute. During this step, function declaration bindings are initialized to their function objects, var bindings are initialized to undefined, and let and const bindings are created but remain uninitialized until their declarations are evaluated.

Once declaration instantiation completes, the engine evaluates the statement list associated with that execution context.

Although many explanations refer to a “creation phase” and an “execution phase,” the specification models execution more precisely as:

  1. creating and pushing an execution context onto the execution context stack
  2. performing declaration instantiation
  3. evaluating the statement list
For functions, [[Call]] leads to FunctionDeclarationInstantiation, and for scripts, ScriptEvaluation leads to GlobalDeclarationInstantiation. These declaration instantiation steps run before any statements are evaluated in their respective execution contexts.
JavaScript
function foo() {
  debugger
  // 1. foo is the running execution context

  function bar() {
    // 3. foo is suspended and bar is the running execution context
    debugger
    console.log('bar')
  }

  bar() // 2. a new execution context is pushed onto the callstack

  // 4. bar is popped off the callstack and foo is the running execution context
  console.log('foo')
}

foo()

Types of Execution Context

There are different types of execution contexts, but they all serve the same purpose: tracking the runtime evaluation of code.

In the spec ECMAScript code execution contexts is just a more specific execution context that runs actual JavaScript source code and therefore needs LexicalEnvironment and VariableEnvironment state components.

Global Execution Context

Any time code is evaluated, there is always a global execution context, such as when executing a for loop in global scope.

The global execution context is created when a script is evaluated as part of ScriptEvaluation and represents execution of code at the top level. There is at most one global execution context per script, and in DevTools it appears in the call stack as (anonymous).

Although the global execution context is often credited with setting up the global environment, that work is performed earlier by the host during realm initialization (InitializeHostDefinedRealm), before ScriptEvaluation creates the global execution context.

Before any global execution context exists, the host creates an original execution context to serve as the running execution context while the realm, global object, and global environment are initialized (InitializeHostDefinedRealm). This initialization is internal, and the original execution context is not visible in the DevTools call stack; only the global execution context appears there as (anonymous).

Function Execution Context

A function execution context is created whenever a function is invoked. Each function invocation gets its own execution context, which is pushed onto the execution context stack when the function begins execution and popped off the stack when the function returns. Function execution contexts are created as part of the PrepareForOrdinaryCall operation.

Execution Context Components

Each execution context contains certain state components which are pointers to environment records. Two of these state components are the LexicalEnvironment and the VariableEnvironment.

Lexical Environment

The LexicalEnvironment state component of the execution context identifies the environment record used to resolve identifier references made by code within this execution context.

An environment record defines the association between identifiers and their bindings according to the lexical nesting structure of the source code. Each environment record has an [[OuterEnv]] field which is null for the global scope or a reference to an outer environment record.

Variable Environment

The VariableEnvironment state component of the execution context is a pointer to the Environment Record that holds bindings created by var declarations within this execution context.

let and const belong to the LexicalEnvironment, not the VariableEnvironment.

Environment Records

An environment record is the runtime representation of a scope. Lexical scope is determined at compile time during semantic analysis, environment records are created at runtime based on that structure.

The different types of environment records are detailed in the spec.

Whenever a declaration is evaluated, such as const, let, or function, the engine creates a binding for the declared identifier in an appropriate environment record. Which type of environment record is used depends on where the declaration appears (global, function, block, or module) and on the kind of declaration.

A binding (sometimes called an identifier binding) associates an identifier name with a storage location managed by the JavaScript engine. These bindings are stored in environment records and are used during identifier resolution to determine the binding associated with a given identifier in order to access or update the value stored in that location.

In the code below, someLetter is an identifier. When execution begins, a binding for someLetter is created in the appropriate environment record with an uninitialized value. When code execution reaches the declaration, the binding’s value is initialized to 'a'.

A variable is an informal term for an identifier that has a binding in an environment record. A binding is the association between that identifier and a storage location, which holds the identifier’s value.

When an identifier is evaluated, the runtime invokes the abstract operation ResolveBinding, which searches the current lexical environment and its outer environments to produce a Reference Record pointing to the appropriate binding.
JavaScript
const someLetter = 'a'
var someNumber = 1

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

Here is a small and simple pseudocode example to help visualize this concept.

JavaScript
GlobalExecutionContext = {
  LexicalEnvironment: GlobalEnvironmentRecord,
  VariableEnvironment: GlobalEnvironmentRecord
}

GlobalEnvironmentRecord = {
  Type: GlobalEnvironmentRecord,
  [[ObjectRecord]]: {
    Type: ObjectEnvironmentRecord,
    [[BindingObject]]: <global object>,
    Bindings: {
      someNumber: { [[Value]]: 1 },
      foo: { [[Value]]: <foo function object> }
    },
    [[OuterEnv]]: null
  },
  [[DeclarativeRecord]]: {
    Type: DeclarativeEnvironmentRecord,
    Bindings: {
      someLetter: { [[Value]]: 'a' },
    },
    [[OuterEnv]]: null
  },
  [[GlobalThisValue]]: <global object>,
  [[OuterEnv]]: null
}

For Loops do not create a new execution context

Evaluating a for loop does not create a new execution context. For for statements with lexical declarations, a new environment record is created for each iteration, and identifier bindings are created in that environment record. During each iteration, the running execution context’s LexicalEnvironment is set to the newly created environment record, resulting in new identifier bindings for let and const on each iteration.

When evaluation of the for loop completes, the running execution context’s LexicalEnvironment is set to the environment record before evaluation of the for loop began.

When a for loop appears in global scope, it is evaluated within the global execution context. No additional execution contexts are created, only the global execution context’s LexicalEnvironment is set to the new environment record created for each iteration.

The .forEach() method executes a provided function once for each array element. Therefore, for each callback invocation a new execution context is created along with a new environment record for that execution context.

Blocks do not create a new execution context

When a block is evaluated, a new environment record is created, and identifier bindings are created in that environment record. The running execution context's lexical environment is set to this newly created environment record. Once evaluation for the block is complete, the running execution context's lexical environment is set to the previous environment record before evaluation of the block began.

JavaScript
function foo() {
  const x = 1

  if (x) {
    // a new environment record is created for this block
    const y = 2
  }

  {
    // a new environment record is also created here
    const x = 2
  }
}

Code Execution Example

At this point, the compilation phase has finished and executable code has been generated. The JavaScript engine will create an execution context for this block of code.

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

function foo() {
  const a = 1

  if (a > 0) {
    const a = 3
    return a + b + c
  }
}

foo()

The JavaScript engine creates a new execution context along with a new environment record and pushes it onto the execution context stack. It then performs GlobalDeclarationInstantiation, which creates and initializes bindings in that environment record. Function declarations are initialized to function objects, var bindings are initialized to undefined, and let and const bindings are created but remain uninitialized until their declarations are evaluated.

GlobalExecutionContext = {
  LexicalEnvironment: GlobalEnvironmentRecord,
  VariableEnvironment: GlobalEnvironmentRecord
}

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

After the bindings have been initialized from GlobalDeclarationInstantiation, the engine evaluates the statement list associated with that execution context.

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

function foo() {
  const a = 1

  if (a > 0) {
    const a = 3
    return a + b + c
  }
}

foo()
GlobalExecutionContext = {
  LexicalEnvironment: GlobalEnvironmentRecord,
  VariableEnvironment: GlobalEnvironmentRecord
}

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

function foo() {
  const a = 1

  if (a > 0) {
    const a = 3
    return a + b + c
  }
}

foo()
GlobalExecutionContext = {
  LexicalEnvironment: GlobalEnvironmentRecord,
  VariableEnvironment: GlobalEnvironmentRecord
}

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

function foo() {
  const a = 1

  if (a > 0) {
    const a = 3
    return a + b + c
  }
}

foo()
GlobalExecutionContext = {
  LexicalEnvironment: GlobalEnvironmentRecord,
  VariableEnvironment: GlobalEnvironmentRecord
}

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

There is nothing to do here since foo is already initialized.

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

function foo() {
  const a = 1

  if (a > 0) {
    const a = 3
    return a + b + c
  }
}

foo()

Evaluation reaches a function invocation and the foo function object’s internal [[Call]] method is invoked. A new function execution context is created with a new function environment record and pushed onto the execution context stack. FunctionDeclarationInstantiation is then performed, creating and initializing bindings in the function environment record.

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

function foo() {
  const a = 1

  if (a > 0) {
    const a = 3
    return a + b + c
  }
}

foo()
FooExecutionContext = {
  LexicalEnvironment: FooEnvironmentRecord,
  VariableEnvironment: FooEnvironmentRecord
}

FooEnvironmentRecord = {
  type: FunctionEnvironmentRecord,
  Bindings: {
    a: { [[Value]]: 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>, // e.g., window in browsers
    Bindings: {
      // Built-in globals (e.g., Math, Array, JSON, etc.)
      c: { [[Value]]: 3 },
      foo: { [[Value]]: <foo function object>
    },
    [[OuterEnv]]: null
  },
  [[DeclarativeRecord]]: {
    Type: DeclarativeEnvironmentRecord,
    Bindings: {
      a: { [[Value]]: 1 },
      b: { [[Value]]: 2 },
    },
    [[OuterEnv]]: null
  },
  [[GlobalThisValue]]: <global object>,
  [[OuterEnv]]: null
}

After the bindings have been initialized from FunctionDeclarationInstantiation, the engine evaluates the statement list associated with that function execution context.

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

function foo() {
  const a = 1

  if (a > 0) {
    const a = 3
    return a + b + c
  }
}

foo()
FooExecutionContext = {
  LexicalEnvironment: FooEnvironmentRecord,
  VariableEnvironment: FooEnvironmentRecord
}

FooEnvironmentRecord = {
  type: FunctionEnvironmentRecord,
  Bindings: {
    a: { [[Value]]: 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>, // e.g., window in browsers
    Bindings: {
      // Built-in globals (e.g., Math, Array, JSON, etc.)
      c: { [[Value]]: 3 },
      foo: { [[Value]]: <foo function object>
    },
    [[OuterEnv]]: null
  },
  [[DeclarativeRecord]]: {
    Type: DeclarativeEnvironmentRecord,
    Bindings: {
      a: { [[Value]]: 1 },
      b: { [[Value]]: 2 },
    },
    [[OuterEnv]]: null
  },
  [[GlobalThisValue]]: <global object>,
  [[OuterEnv]]: null
}
JavaScript
const a = 1
let b = 2
var c = 3

function foo() {
  const a = 1

  if (a > 0) {
    const a = 3
    return a + b + c
  }
}

foo()

The if (a > 0) condition evaluates to true and the code inside the block is evaluated.

FooExecutionContext = {
  LexicalEnvironment: FooBlockEnvironmentRecord,
  VariableEnvironment: FooEnvironmentRecord
}

FooBlockEnvironmentRecord = {
  type: DeclarativeEnvironmentRecord,
  Bindings: {
    a: { [[Value]]: uninitialized }
  },
  [[OuterEnv]]: FooEnvironmentRecord
}

FooEnvironmentRecord = {
  type: FunctionEnvironmentRecord,
  Bindings: {
    a: { [[Value]]: 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>, // e.g., window in browsers
    Bindings: {
      // Built-in globals (e.g., Math, Array, JSON, etc.)
      c: { [[Value]]: 3 },
      foo: { [[Value]]: <foo function object>
    },
    [[OuterEnv]]: null
  },
  [[DeclarativeRecord]]: {
    Type: DeclarativeEnvironmentRecord,
    Bindings: {
      a: { [[Value]]: 1 },
      b: { [[Value]]: 2 },
    },
    [[OuterEnv]]: null
  },
  [[GlobalThisValue]]: <global object>,
  [[OuterEnv]]: null
}
JavaScript
const a = 1
let b = 2
var c = 3

function foo() {
  const a = 1

  if (a > 0) {
    const a = 3
    return a + b + c
  }
}

foo()
FooExecutionContext = {
  LexicalEnvironment: FooBlockEnvironmentRecord,
  VariableEnvironment: FooEnvironmentRecord
}

FooBlockEnvironmentRecord = {
  type: DeclarativeEnvironmentRecord,
  Bindings: {
    a: { [[Value]]: 3 }
  },
  [[OuterEnv]]: FooEnvironmentRecord
}

FooEnvironmentRecord = {
  type: FunctionEnvironmentRecord,
  Bindings: {
    a: { [[Value]]: 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>, // e.g., window in browsers
    Bindings: {
      // Built-in globals (e.g., Math, Array, JSON, etc.)
      c: { [[Value]]: 3 },
      foo: { [[Value]]: <foo function object>
    },
    [[OuterEnv]]: null
  },
  [[DeclarativeRecord]]: {
    Type: DeclarativeEnvironmentRecord,
    Bindings: {
      a: { [[Value]]: 1 },
      b: { [[Value]]: 2 },
    },
    [[OuterEnv]]: null
  },
  [[GlobalThisValue]]: <global object>,
  [[OuterEnv]]: null
}

The return statement causes the function to cease evaluation and the value 8 is returned. The foo execution context is removed from the execution context stack and the global execution context is the running execution context.

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

function foo() {
  const a = 1

  if (a > 0) {
    const a = 3
    return a + b + c
  }
}

foo()
GlobalExecutionContext = {
  LexicalEnvironment: GlobalEnvironmentRecord,
  VariableEnvironment: GlobalEnvironmentRecord
}

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

function foo() {
  const a = 1

  if (a > 0) {
    const a = 3
    return a + b + c
  }
}

foo()

There is nothing left for the global execution context to evaluate in the script so the global execution context is removed from the execution context stack and code execution is complete.

Each time foo is invoked, the entire process repeats. A new execution context and its environment record are instantiated, and identifier bindings are created in that environment record. When the execution context is removed from the execution context stack, its environment record becomes unreachable and is eligible for garbage collection, and all the bindings associated with that environment record cease to exist, except in the case of closures.

Conclusion

Execution contexts are a foundational concept and are key to understanding scope, hoisting, the temporal dead zone, and the this keyword. Before any code is executed, the JavaScript engine performs a compilation step where the lexical structure of the code is analyzed and scopes are identified during semantic analysis. JavaScript is lexically scoped, meaning the physical structure of the code determines variable and function visibility. When execution begins, compiled code is evaluated within execution contexts, and the scopes identified during compilation are instantiated as environment records.