Javascript Execution Context

This article is going to cover some difficult concepts but it is crucial to understand as it will lay the foundation for everything else. Understanding the concepts covered in this article will allow you to understand scope, closures, this keyword and hoisting in depth.

ECMAScript is the official javascript specification, it describes how ECMAScript should behave and javascript is an implementation. It is the job of engineers that build a javascript engine, such as the team of engineers that built the chrome v8 engine, to follow the spec to a T. For this reason, if you want to know how javascript works down to the last detail it's important to start reading the spec.

The concepts that will be covered here cannot be accessed with the javascript language itself. Javascript indeed has a lexical environment, but this isn't something that you have access to or that you can really view. It's the way the javascript engine handles the code to be executed internally. It's a bit like driving a car when writing javascript, you have access to the brake, throttle and steering wheel but you can't access the pistons inside the engine or the internal methods of the javascript engine, after all the javascript engine executes javascript but isn't written in javascript itself. In the case of the chrome v8 engine, it's written in c++.

How does the JavaScript Engine Execute Code?

The first thing we need to talk about cover before we get into execution contexts, is how the javascript engine executes code. This is a process that happens in two phases:

  1. Compilation/parsing phase
  2. Execution phase

When reading the spec, there will be terms static sematics and runtime semantics. These refer to operations that run during the compilation phase and execution phase, respectively.

A javascript program is always processed in these two phases, this can be proved in the code block below.

JavaScript
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 but this is not the case. This gets a little more interesting if we have a function that is not invoked.

javascript
let a = 'a'
console.log(a)

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

What do you think will happen here? Notice foo is not invoked, but this does not matter, the error was still caught. The reason it was caught because the code was first scanned.

Here is another example, we will declare let twice in the same scope and not invoke the funtion.

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

This should make it very clear that there is indeed two phases, if there is an error no code execution will begin.

In this compilation phase, all the scopes are identified and variables are also mapped to their respective scopes. The scopes and variables will be instantiated in the execution phase.

This is where the term Lexical Scope comes from and it's called lexical scope because it has to do with the lexing stage of compilation. Lexical Scope is determined by the physical location of code in the program during the lexing phase.

During the parsing phase of compilation, the code is scanned and a map of the scopes is created, also known as an Abstract Syntax Tree or AST, this map will be used when the program executes. As this code is being compiled, it associates const x = 1 with a function scope and const x = 2 with a block scope. If there was another variable being declared in the block with the identifier x ie. const x = 2, then the compiler would complain and throw an error. Remember, the below function scope and block scope have been identified, but not instantiated as no memory for them has been reserved. In other words, the code was scanned, scopes were identified, and variables were registered to their respective scopes but varialbes are not created until runtime.

JavaScript
function foo() {
  const x = 1

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

Once the compiler begins the parsing phase and creates an AST, it would look something like this for the above code block.

{
  "type": "Program",
  "start": 0,
  "end": 88,
  "body": [
    {
      "type": "FunctionDeclaration",
      "start": 1,
      "end": 88,
      "id": {
        "type": "Identifier",
        "start": 10,
        "end": 13,
        "name": "foo"
      },
      "expression": false,
      "generator": false,
      "async": false,
      "params": [],
      "body": {
        "type": "BlockStatement",
        "start": 16,
        "end": 88,
        "body": [
          {
            "type": "VariableDeclaration",
            "start": 20,
            "end": 31,
            "declarations": [
              {
                "type": "VariableDeclarator",
                "start": 26,
                "end": 31,
                "id": {
                  "type": "Identifier",
                  "start": 26,
                  "end": 27,
                  "name": "x"
                },
                "init": {
                  "type": "Literal",
                  "start": 30,
                  "end": 31,
                  "value": 1,
                  "raw": "1"
                }
              }
            ],
            "kind": "const"
          },
          {
            "type": "IfStatement",
            "start": 35,
            "end": 86,
            "test": {
              "type": "BinaryExpression",
              "start": 39,
              "end": 44,
              "left": {
                "type": "Identifier",
                "start": 39,
                "end": 40,
                "name": "x"
              },
              "operator": ">",
              "right": {
                "type": "Literal",
                "start": 43,
                "end": 44,
                "value": 0,
                "raw": "0"
              }
            },
            "consequent": {
              "type": "BlockStatement",
              "start": 46,
              "end": 86,
              "body": [
                {
                  "type": "VariableDeclaration",
                  "start": 52,
                  "end": 63,
                  "declarations": [
                    {
                      "type": "VariableDeclarator",
                      "start": 58,
                      "end": 63,
                      "id": {
                        "type": "Identifier",
                        "start": 58,
                        "end": 59,
                        "name": "x"
                      },
                      "init": {
                        "type": "Literal",
                        "start": 62,
                        "end": 63,
                        "value": 2,
                        "raw": "2"
                      }
                    }
                  ],
                  "kind": "const"
                },
                {
                  "type": "ExpressionStatement",
                  "start": 68,
                  "end": 82,
                  "expression": {
                    "type": "CallExpression",
                    "start": 68,
                    "end": 82,
                    "callee": {
                      "type": "MemberExpression",
                      "start": 68,
                      "end": 79,
                      "object": {
                        "type": "Identifier",
                        "start": 68,
                        "end": 75,
                        "name": "console"
                      },
                      "property": {
                        "type": "Identifier",
                        "start": 76,
                        "end": 79,
                        "name": "log"
                      },
                      "computed": false,
                      "optional": false
                    },
                    "arguments": [
                      {
                        "type": "Identifier",
                        "start": 80,
                        "end": 81,
                        "name": "x"
                      }
                    ],
                    "optional": false
                  }
                }
              ]
            },
            "alternate": null
          }
        ]
      }
    }
  ],
  "sourceType": "module"
}

What is an Execution Context?

The execution phase of the javascript engine uses devices called execution contexts to track the runtime evaluation of your code.

Anytime a function is invoked in javascript, a new execution context is created and pushed onto the top of the execution context stack, or callstack. When viewing the callstack using a debugger statement, each stack frame in the call stack is indeed an execution context. The topmost entry in the callstack is referred to by the spec as the running execution context. The job of the callstack is to track the execution contexts.

There is always at least the global execution context in the callstack, represented by (anonymous) at the bottom of the callstack. In the case of a forEach loop, for each iteration a new execution context is created, evaluated and popped off the callstack. When the code finishes the global execution context is also popped and the callstack is empty. With a for loop in global scope, there will still be a global execution context created and for each iteration, a new environment record (scope) will be instantiated, but since there is no callback, for each iteration there is not a new execution context.

When the below code is evaluated in the console, (anonymous) is in the call stack pane and on top of that you will see foo.

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

foo()

At any point in time, the running execution context may be suspended and a different execution context will become the running execution context, for example:

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.

  // 4. foo is the running execution context
  console.log('foo')
}

foo()

Execution Context Components

Each execution context has a few state components associated with it to track the execution progress of it's associated code. The main ones to take a look at are the LexicalEnvironment and VariableComponent components.

LexicalEnvironment

The LexicalEnvironment state component of the execution context is a pointer to an Environment Record used to resolve identifier references, ie. let or const, made by code within this execution context.

An Environment Record is used to define the association of identifiers to specific variables and functions based upon the lexical nesting structure of the code. Each Environment Record has an [[OuterEnv]] field which is null for the global scope or a reference to an outer environment record.

VariableEnvironment

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.

Environment Record (scope)

There are a few different types of environment records, but since this article covers some abstract concepts I will go over the Declarative Environment Record and the Object Environment Record. Understanding these concepts will help us more than enough to understand how js programs are executed and since a lot of what we are covering is just a description of an implementation, there is no one right way to describe all of this in pseudocode.

Note that the Global Environment Record is a composite environment record, made up of a Declarative Environment Record and an Object Environment Record.

Declarative Environment Record

From the spec

Each Declarative Environment Record is associated with an ECMAScript program scope containing variable, constant, let, class, module, import, and/or function declarations. A Declarative Environment Record binds the set of identifiers defined by the declarations contained within its scope.

What this means is that any time there is a declaration made, for example const someLetter = 'a' with const, let, function or anything else listed above, there is a Declarative Environment Record associated with it, and it binds the identifiers to their values.

Note that there are two subclasses of Declarative Environment Record

  1. Function Environment Record
  2. Module Environment Record

Object Environment Record

The Object Environment Record is associated with an object called its binding object. Its binding object is the global object, which is window in the case of browsers.

Global Environment Record

The Global Environment Record is a composite that wraps a Declarative Environment Record and an Object Environment Record.

It has the following fields:

  • [[ObjectRecord]] - pointer to the binding object
  • [[GlobalThisValue]]
  • [[DeclarativeRecord]]
  • [[VarNames]]

Types of Execution Context

For the sake of simplicity there is also an eval execution context but I'm only going to talk about the Global Execution Context and the Function Execution Context. A global execution context is simply an execution context associated with global code and a function execution context is an execution context associated with function code.

Original Execution Context

Before a global execution context is created, there is actually an execution context that is created first, this is not visible in the dev tools call stack pane, we can only see (anonymous), or the global execution context at the bottom of the stack, but below that is another execution context.

You may often see the global execution context being credited with setting up the global environment, but this is not true. Before the global execution context is created in ScriptEvaluation, an operation runs which creates and sets up the global environment and global object. The name of this operation is InitializeHostDefinedRealm, the operation responsible for creating this original execution context we can't see.

Nowhere else in the spec is InitializeHostDefinedRealm called, this operation acts as an entry point for a hosting environment.

Notice on line 9 what happens, SetRealmGlobalObject is executed, then inside that operation the operation NewGlobalEnvironment is called and that returns a new global environment record.

9.6 InitializeHostDefinedRealm()

  1. Let realm be CreateRealm().
  2. Let newContext be a new execution context.
  3. Set the Function of newContext to null.
  4. Set the Realm of newContext to realm.
  5. Set the ScriptOrModule of newContext to null.
  6. Push newContext onto the execution context stack; newContext is now the running execution context.
  7. If the host requires use of an exotic object to serve as realm's global object, let global be such an object created in a host-defined manner. Otherwise, let global be undefined, indicating that an ordinary object should be created as the global object.
  8. If the host requires that the this binding in realm's global scope return an object other than the global object, let thisValue be such an object created in a host-defined manner. Otherwise, let thisValue be undefined, indicating that realm's global this binding should be the global object.
  9. Perform SetRealmGlobalObject(realm, global, thisValue).
  10. Let globalObj be ? SetDefaultGlobalBindings(realm).
  11. Create any host-defined global object properties on globalObj.
  12. Return unused.

9.3.3 SetRealmGlobalObject ( realmRec, globalObj, thisValue )

  1. If globalObj is undefined, then
    1. Let intrinsics be realmRec.[[Intrinsics]].
    2. Set globalObj to OrdinaryObjectCreate(intrinsics.[[%Object.prototype%]]).
  2. Assert: globalObj is an Object.
  3. If thisValue is undefined, set thisValue to globalObj.
  4. Set realmRec.[[GlobalObject]] to globalObj.
  5. Let newGlobalEnv be NewGlobalEnvironment(globalObj, thisValue).
  6. Set realmRec.[[GlobalEnv]] to newGlobalEnv.
  7. Return unused.

9.1.2.5 NewGlobalEnvironment ( G, thisValue )

  1. Let objRec be NewObjectEnvironment(G, false, null).
  2. Let dclRec be NewDeclarativeEnvironment(null).
  3. Let env be a new Global Environment Record.
  4. Set env.[[ObjectRecord]] to objRec.
  5. Set env.[[GlobalThisValue]] to thisValue.
  6. Set env.[[DeclarativeRecord]] to dclRec.
  7. Set env.[[VarNames]] to a new empty List.
  8. Set env.[[OuterEnv]] to null.
  9. Return env.

Review

  • InitializeHostDefinedRealm is an entry point for the hosting environment, it is not called anywhere in the spec
    • It creates an execution context, not an ECMAScript code execution context
    • It pushes this execution context onto the execution context stack which becomes the running execution context
    • It calls SetRealmGlobalObject
    • It sets any host defined global object properties on the global object
  • SetRealmGlobalObject sets up the environment for the realm
    • It sets the realm record's global object, [[GlobalObj]], to the global object
    • It sets the realm record's global environment, [[GlobalEnv]], to the return of calling NewGlobalEnvironment
  • NewGlobalEnvironment returns a new global environment record, or global scope.
  • The Global Environment Record has the following fields
    • [[ObjectRecord]]
    • [[GlobalThisValue]]
    • [[DeclarativeValue]]
    • [[VarNames]]

Global Execution Context

This is a lot of information but the main takeaway should be that there is actually an execution context that sits below the global execution context in the call stack that we can't see. It is not visible using a debugger statement and we also don't need to see it. It's only an entry point for the hosting environment.

Every execution context has a field [[ScriptOrModule]], and since the execution context created above is the original execution context, it's [[ScriptOrModule]] value is null. When the global execution context is created in ScriptEvaluation, then this value will be the Script Record from which the code originates. Every execution context has a field [[ScriptOrModule]], and since the execution context created above is the original execution context

Now let's see how the global execution context is created.

This is very important, notice step 1, globalEnv is scriptRecord.[[Realm]].[[GlobalEnv]]. In InitializeHostDefinedRealm a new Realm Record is created with CreateRealm and a Realm Record has a field [[GlobalEnv]] which is the global environment for this realm.

When the global execution context is created on line 2, the VariableEnvironment, LexicalEnvironment and PrivateEnvironment of the scriptContext (global environment record or global scope) are set to the globalEnv on lines 5-7. Now the execution at the bottom of the stack that we can't see is suspended, the new global execution context, scriptContext, is pushed onto the execution context stack and becomes the running execution context. If this were in chrome dev tools, you would only see this new global execution context as (anonymous) and the execution context that sits below it is not visible.

scriptContext is indeed a global execution context, and step 9 confirms there is a running execution that sits below the global execution context, if there wasn't anything below it, there would be nothing to suspend.

16.1.6 ScriptEvaluation (scriptRecord)

  1. Let globalEnv be scriptRecord.[[Realm]].[[GlobalEnv]].
  2. Let scriptContext be a new ECMAScript code execution context.
  3. Set the Function of scriptContext to null.
  4. Set the Realm of scriptContext to scriptRecord.[[Realm]].
  5. Set the ScriptOrModule of scriptContext to scriptRecord.
  6. Set the VariableEnvironment of scriptContext to globalEnv.
  7. Set the LexicalEnvironment of scriptContext to globalEnv.
  8. Set the PrivateEnvironment of scriptContext to null.
  9. Suspend the running execution context.
  10. Push scriptContext onto the execution context stack; scriptContext is now the running execution context.
  11. Let script be scriptRecord.[[ECMAScriptCode]].
  12. Let result be Completion(GlobalDeclarationInstantiation(script, globalEnv)).
  13. If result.[[Type]] is normal, then
    1. Set result to Completion(Evaluation of script).
    2. If result.[[Type]] is normal and result.[[Value]] is empty, then
      1. Set result to NormalCompletion(undefined).
  14. Suspend scriptContext and remove it from the execution context stack.
  15. Assert: The execution context stack is not empty.
  16. Resume the context that is now on the top of the execution context stack as the running execution context.
  17. Return ? result.

Function Execution Context

A function execution context is created any time a function is invoked and there can be any number of function execution contexts in a javascript program.

From the spec:

A new execution context is created whenever control is transferred from the executable code associated with the currently running execution context to executable code that is not associated with that execution context. The newly created execution context is pushed onto the stack and becomes the running execution context.

The spec also details what happens when an execution context is created for evaluating a function in the section on FunctionDeclarationInstantiation.

When an execution context is established for evaluating an ECMAScript function a new Function Environment Record is created and bindings for each formal parameter are instantiated in that Environment Record. Each declaration in the function body is also instantiated. If the function's formal parameters do not include any default value initializers then the body declarations are instantiated in the same Environment Record as the parameters. If default value parameter initializers exist, a second Environment Record is created for the body declarations. Formal parameters and functions are initialized as part of FunctionDeclarationInstantiation. All other bindings are initialized during evaluation of the function body.

The spec details how a function execution context is created below. On line 2 a new function execution context is created. On line 7, localEnv is a new Function Envrionment Record or function (local) scope. Also note that Function Environment Records are also Declarative Environment Records, they are just a subclass.

10.2.1.1 PrepareForOrdinaryCall ( F, newTarget )

The abstract operation PrepareForOrdinaryCall takes arguments F (a function object) and newTarget (an Object or undefined) and returns an execution context. It performs the following steps when called:

  1. Let callerContext be the running execution context.
  2. Let calleeContext be a new ECMAScript code execution context.
  3. Set the Function of calleeContext to F.
  4. Let calleeRealm be F.[[Realm]].
  5. Set the Realm of calleeContext to calleeRealm.
  6. Set the ScriptOrModule of calleeContext to F.[[ScriptOrModule]].
  7. Let localEnv be NewFunctionEnvironment(F, newTarget).
  8. Set the LexicalEnvironment of calleeContext to localEnv.
  9. Set the VariableEnvironment of calleeContext to localEnv.
  10. Set the PrivateEnvironment of calleeContext to F.[[PrivateEnvironment]].
  11. If callerContext is not already suspended, suspend callerContext.
  12. Push calleeContext onto the execution context stack; calleeContext is now the running execution context.
  13. NOTE: Any exception objects produced after this point are associated with calleeRealm.
  14. Return calleeContext.

Example

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()

During runtime, the engine will consult the AST or Abstract Syntax Tree from the parsing phase of compilation. The lexing/tokenization (lexical analysis) phase of compilation identified all the scopes and registered variables to the respective scopes. The next stage of compilation is parsing (syntax analysis), this is where the AST is created which is basically a set of instructions for runtime. More technically, the AST is usually the result of the parsing phase of the compiler and serves as an intermediate representation of the program.

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

  1. A global execution context will be created
  2. The LexicalEnvironment and VariableEnvrionment components of the global execution context will be set up and pointed to the global environment record (scope)
  3. The running execution context will be suspended. This running execution context is the original execution context we talked about earlier that was responsible for setting up the global object, global bindings, ect
  4. The global execution context will be pushed onto the execution context stack and will become the running execution context.
  5. All the global declarations will be instantiated
    1. creates bindings that are uninitialized for let and const.
    2. if there are functions to initialize, instantiates the function objects.
      1. adds all the internal methods the function object needs to be considered a function. [[Call]] for example.
    3. creates a mutable binding in the Object Environment Record, meaning the function declaration is accessible on the window, and initializes to its function object which was returned from the previous step.
    4. creates mutable bindings that are initialized to undefined for var declarations in the object environment record. If it already exists, it's reused and assumed to be intialized.
  6. Code evaluation will begin and when a declaration is evaluated, the binding will be intialized for let and const, for var the binding will be re-assigned to the respective value.
  7. after the code has finished evaluating, suspend the global execution context and and remove it from the global execution context stack. the original execution context is now the running execution context.
  8. if there is a value return it

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

  1. During runtime if a set of parenthesis ( ) is encountered, the internal [[Call]] method of the function object is called.
  2. a new function execution context is created.
  3. a new function environment record or function scope is created
  4. the LexicalEnvironment and VariableEnvironment are pointed to the function environment record which was just created.
  5. running execution context is suspended.
  6. this new execution context is pushed onto the execution context stack or callstack
  7. this value is set (OrdinaryCallBindThis)
  8. creates bindings that are uninitialized for let and const.
  9. if there are functions to initialize, instantiates the function objects.
    1. adds all the internal methods the function object needs to be considered a function. [[Call]] for example.
    2. creates a mutable binding in the Object Environment Record, meaning the function declaration is accessible on the window, and initializes to its function object which was returned from the previous step.
  10. creates mutable bindings that are initialized to undefined for var declarations in the object environment record. If it already exists, it's reused and assumed to be intialized.
  11. code evaluation will begin and when a declaration is evalutated, the binding will be initialized for let and const, for var the binding will be re-assigned to the respective value.
  12. remove this execution context from the execution context stack and restore the previous execution context as the running execution context.
  13. if there is a return value, return it

Taking into account what we just learned, this is how the javascript engine views the code block from above. A global execution context will be created and our program looks like this. Image the below code block as the execution context stack. New execution contexts are appended to the top. After the js engine compiles the code and parses it, it will look something like this.

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

Everything is now setup, all the declarations are hoisted, including let and const, they are just uninitialized and evaluation begins. Once line 1 is evaluated the const a = 1 variable is initialized through assignment 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 and b is initialized through assignment.

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

line 3 is evaluated and re-assigned to 3

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

line 14 is evaluated. There is nothing to do on line 10 because the function object was already created and registered in the environment record. On line 14, 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 object's internal [[Call]] method, this creates a new exeuction context, instantiates a new function environment (function scope), points the execution context's LexicalEnvironment and VariableEnvironment state components to the newly instantiated function environment record, suspends the running execution context, this new execution context is pushed onto the the execution context stack and becomes the running execution context, all the variables that were registered to the respected scope by looking at the AST from the parsing stage of compilation are created and code will start to evaluate.

Variables and scope have been instantiated and the program looks like this right before evaluation begins. Remember that the Abstract Syntax Tree (AST) acts as a runtime blue print. It maps all the scopes and variables but doesn't allocate memory or instantiate anything until runtime.

// appended to the top of the execution context stack
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: undefined,
    [[OuterEnv]]: null
  }
}

line 6 is evaluated and const a is initialized through assigment to 1.

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

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

line 8 evaluates to true so the code inside the block is executed. What happens next is block evaluation, there is no new execution context created.

A new declarative environment record (scope) is created, bindings for let and const are created that are unintialized, the running execution context's LexicalEnvironment will be pointed to the new environment record that was just created, the block will begin to be evaluated.

Right before the block begins evaluation, it will look like this:

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: undefined,
    [[OuterEnv]]: null
  }
}

Evaluation begins and line 9 is evaluated.

FunctionExecutionContext = {
  LexicalEnvironment: {
    a: 3
  }
}

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

line 10 is evaluated, the return statement causes the function to cease evaluation and returns the value 8. foo is removed from the execution context stack and the global execution context is restored as the running execution context. There is nothing more for the global execution context to evaluate so it is also removed from the execution context stack.

One important thing to note, if foo were called a second time, on line 15 for example, the whole process would repeat all over again, each time the function foo is invoked, a new execution context is created and variables are created, hoisted, and initialized all over again. When the code for the associated execution context finishes being evaluated, it is removed from the execution context stack and is garbage collected, except in the case of closures.

Conclusion

Each time a function is invoked an execution context is created and appended to the top of the execution context stack or call stack. Anytime any code is evaluted, such as a block or for loop in global scope, there is always at least the global execution context. Blocks and for loops do not create a new execution context but they do create new scope instances.

JavaScript code is evaluated in two phases

  1. Compilation/parsing
  2. Execution

The spec refers to compile time operations as static semantics and run time operations runtime semantics

There are two types of Execution Context

  • Global Execution Context
  • Function Execution Context

An execution context contains LexicalEnvironment and VariableEnvironment state components.

The LexicalEnvironment is a pointer to an Environment Record (scope)

There are three types of EnvironmentRecord

  1. Declarative Environment Record which also has two subclasses.
    • Function Environment Record
    • Module Environment Record
  2. Object Environment Record
  3. Global Environment Record

A few things to note as we conclude, in a forEach loop, for every iteration, a new execution context is created, variables are created, code is evaluated and the execution context is removed from the execution context stack. This process repeats for each iteration of Arrya.prototyp.forEach() because for every element in the array a callback is invoked which creates a new execution context.

A for loop in global scope will be evaluated in a global execution context because any time any code is evaluated, there is always at least the global scope. For each iteration of the for loop, there will be a new Environment Record or scope created and the global execution context's lexical environment component will be pointed to the new Environment Record created for each iteration. That means for each iteration there are brand new variables being created for let and const. The article on scope covers for loops and scope thoroughly.

When the javascript engine is evaluating code and comes accross a function reference with parenthesis (), the internal [[Call]] method of the function object will be invoked. This runtime operation creates a new function execution context, creates a new Function Environment Record, or function scope, creates the variables and hoists them, and right before evaluation begins, the this value will be set with the abstract operation OrdinaryCallBindThis.

When a block is evaluated, a new Declarative Environment Record will be created and the running execution context's LexicalEnvironment state component will be pointed to the new Declarative Environment Record. When evaluation leaves the block, the running execution context's LexicalEnvironment component will be pointed back to the previous environment record before the block was evaluated.