Execution Context

Many characteristic behaviors of JavaScript such as Hosting and this binding(🔗 JavaScript this) are caused by the fact that JavaScript engine executes code in order by creating and removing what is called "Execution Context". Understanding what execution context is helps better understanding how core concepts of JavaScripts are interrelated.

Execution Context contains a set of information needed to execute code.

Take the following code, for example.

// main.js

let num1 = 20
const num2 = 30
var subtractor = 40

function doMath(num1, num2) {
  const multiplier = 1
  var divider = 10
  return ((num1 + num2) * multiplier) / divider - subtractor
}

const result = doMath(10, 20)
Figure 1

As JavaScript engine starts executing main.js file from top to bottom, it first creates execution context for JavaScript file itself, namely for the entire program, which is called 'global execution context.' Then, every time a function is called, new environment context is created and put at the top of call stack.
The piece of code that JavaScript engine is currently executes corresponds to the  scope of code whose execution context is currently at the top of call stack. Hence, execution context is also called 'calling stack' or 'current scope.' When a function returns, its execution stack is removed from the stack(even when there is no explicit return in function, it is considered that the function implicitly returned undefined). The engine goes on to execute the stack below. When there is no more execution context in the call stack other than global execution context, global execution context is removed from the stack and the execution of the entire program finishes. By stacking execution context and popping it off in order, JavaScript engine guarantees that JavaScript code is executed in an expectable, orderly manner.

💡 When an execution context context is removed from call stack, the information that it kept (which we will soon refer to as "lexical environment") may or may not be erased from memory. It  depends on whether it is referenced later on(by properties its outer lexical environment). For more, see Closure

Anatomy

According to ECMAScript specification, execution context

"contains whatever state is necessary to track the execution progress of its associated code," (ECMAScript §10.3)

including the following:

ComponentPurpose
LexicalEnvironmentIdentifies the Lexical Environment used to resolve identifier references made by code within this execution context.
VariableEnvironmentIdentifies the Lexical Environment whose environment record holds bindings created by VariableStatements and FunctionDeclarations within this execution context.
ThisBindingThe value associated with the this keyword within ECMAScript code associated with this execution context.

VariableEnvironment is a snapshot of context at the time it was created. Further changes to the context is not updated. LexicalEnvironment is the copy of VariableEnvironment. It starts out with all the informations that VariableEnvironment has. In addition to that, when changes are made in the context, those changes are updated to LexicalEnvironment.

Specification also says that

"an execution context is purely a specification mechanism and need not correspond to any particular artefact of an ECMAScript implementation. It is impossible for an ECMAScript program to access an execution context." (ECMAScript §10.3)

Thus, this post will try to conceptualize execution context as if they are constructed as such, at the cost of not pertaining to the exactly same terminology that appears in the specification. Especially, this post will omit VariableEnvironment part of execution context's interface and only focus on LexicalEnvironment, since it better illustrates how variables are initialized and assigned value in the course of code execution.

As this post sees it, the execution context consists mainly of three parts:

ExecutionContext {
  environmentRecord
  outerEnvironmentRecordReference
  thisBinding
}
  • Environment Record: traversing from top to bottom of code within an execution context, JavaScript engine keeps dictionary record of identifier-variable mapping within the context. Here, identifiers mean the name of variables and variables could be variables declared with let, const or var, functions and also arguments passed to functions.
  • Reference to Outer Environment Record: refers to the environment in which current execution context was created. Reference to outer environment is constructed like a linked list. Therefore, an execution context can only reference its direct outer environment. Such link makes possible what is called 'scope chain'; A function or variable is first searched within innermost environment record and if it doesn't exist, goes on to look up outer environment record, until it reaches the outermost environment record.
  • This Binding: In JavaScript, what this means varies depending on in part of the code it appears. It is part of Execution Context's responsibility to determine how this identifier in a piece of code should be evaluated. To read more about this binding, see also 🔗 JavaScript this

3 Types of Execution Context

Every execution context contains informations as stated above—environment record and reference to outer environment record. However, those information mean different things depending on the types of execution context. There are three types of execution context:

  • Global Execution Context
  • Function Execution Context
  • Eval Function Execution Context

Global Execution Context

Once JavaScript file starts to execute, JavaScript engine creates a global context for the file, namely for the program itself. This global context is called the "global execution context." Global execution context is not inside any function and there can be only one global execution context in a program.

Consider the execution of the following code.

// main.js

var subtractor = 40
let multiplier = 20
const divider = 30

function doMath(num1, num2) {
  const multiplier = 1
  var divider = 10
  return ((num1 + num2) * multifier) / divider - subtractor
}

For the code is not surrounded by any function clause and the doMath function is only declared not called, only global execution context is created. The following is the abstraction of its internals.

When the execution context is created:

GlobalExectionContext implements ExecutionContext {
  environmentRecord: Record<any, any>
  outerEnvironmentRecordReference: null
  thisBinding: GlobalObject
}

globalExecutionContext: GlobalExectionContext = {
  environmentRecord: {
    subtractor: undefined,
    // multiplier: <uninitialized>
    // divider: <uninitizlized>
    doMath: fn
  }
  outerEnvironmentRecordReference: null
  thisBinding: Window
}
  • Environment Record: Note that
    - var variables are initialized with the value undefined
    - const and let variables are uninitialized. It means that "the engine knows about the variable, but it cannot be referenced until" it is later declared by let or const. "It's almost the same as if the variable didn't exist."
    - function declarations are stored in their entirety and are immediately ready to use. cf. Such only holds truth to function declaration, but not to function expressions(e.g. let doMath = function () { ... }), where a function is assigned to a variable.
    For more on initialization and default value assignment of different types of variables, see 🔗 Hoisting.
  • Reference to Outer Environment: Since global context is not declared as a function would be, its outer environment refers to null.
  • this binding: in global execution context, this refers to global object. It is window in browser runtime and global in Node.js runtime.

And then when the code starts to execute:

globalExecutionContext: GlobalExectionContext = {
  environmentRecord: {
    subtractor: 40,
    multiplier: 20,
    divider: 30,
    doMath: fn
  }
  outerEnvironmentRecordReference: null
  thisBinding: Window
}
  • Environment Record: Environment record is updated and all variables are now both initialized and assigned value.

Function Execution Context

Every time a function is called, new execution context is created. There can be as many function execution contexts as there are functions in a program. Suppose that, in the above code snippet, the function add is not only declared but also invoked.

var subtractor = 40
let multiplier = 20
const divider = 30

function doMath(num1, num2) {
  const multiplier = 1
  var divider = 10
  return ((num1 + num2) * multiplier) / divider - subtractor
}

const d = doMath(10, 20) // -37

On top of global context, function execution context for doMath is created, which can be abstracted as follows.

In the creation phase,

FunctionExectionContext implements ExecutionContext {
    environmentRecord: Record<any, any>
    outerEnvironmentRecordReference: GlobalLexicalEnvironment, // GlobalLexicalEnvironment | OuterLexicalEnvironment
    thisBinding: GlobalObject // GlobalObject | undefined
}

doMathFunctionExectionContext: FunctionExectionContext {
    environmentRecord: {
      num1; 10,
      num2: 20,
      multiplier: <uninitialized>,
      divider: <uninitialized>,
      subtractor: undefined
    },
    outerEnvironmentRecordReference: Window
    thisBinding: Window
}

Note the following in function execution context:

  • Environment Record: Not that
    - function execution context adds arguments num1 and num2 to its environment record while in global execution context, there is no arguments.
  • Reference to Outer Environment: Since doMath is one depth below global code, its outer environment record refers to lexical environment of global execution context. If a function is nested many times, it could point to environment record of another function execution context.
  • this binding: if this is not specified, as is the case in current doMath() call, this within a function refers to global object. Depending on how the function is called, this can mean different things.

In the execution phase,

doMathFunctionExectionContext: FunctionExectionContext = {
    environmentRecord: {	
      num1: 10,
      num2: 20,
      multiplier: 1,
      divider: 30,
      subtractor: 40
    },
    outerEnvironmentRecordReference: Window
    thisBinding: Window
}

After the function doMath's execution completes, doMath's function execution context is removed from call stack and the returned value of function is stored to variable d. Global execution context's lexical environment is updated accordingly and identifier-variable mapping res: -37 is added.

  • Scope Chain: Even though the identifier with name subtractor is not defined within the function execution context, the code doesn't break. Due to the context's reference to its outer environment record, if it cannot find subtractor within its own environment record, it looks for subtractor in its outer environment until it reaches the outermost environment record, which is that of the global context(if it still can't find the variable in the global execution context, in strict mode, an error is thrown. Without use strict, new global variable named subtractor is implicitly declared).  Such hierarchy of scope within which an identifier is looked up for from inside to outside is called 'scope chain.'
  • Variable Shadowing: Even though identifiers with name multiplier and divider are already declared in the global execution context, the function execution context's outer context, multiplier and divider referenced arguments passed to doMath function. Since identifiers with name multiplier and divider can be found in doMath function execution context's own environment record, the search for multiplier and divider identifiers stop and does not proceed to the scope chain. This is why doMath(10, 20) evaluates to -37. As such, when identifier with same name is declared both in inner scope and outer scope, the one in inner scope overshadows the one in outer scope. Such phenomena is called 'variable shadowing'.
💡
You can visualize creation and execution phase of execution context with the help of this website(🔗 Python Tutor Code Visualizer)

Eval Function Execution Context

JavaScript eval function turns string into executable JavaScript code. Code executed inside an eval also gets its own execution context. However, eval exposes application to injection attacks by receiving malicious string that can destroy application or database. eval() is deprecated and followingly execution context of eval function will not be convered in this post.

Motivation

As complex a concept execution context is, why is JavaScript designed to work this way? Here are couple of reasons why.

Consider the following code snippet.

let name = 'totally unrelated variable with too broad name declared 5 years ago with'

// (...)

const greet (name) {
  const salutation = 'Hi'
  console.log(`${salutation}, my name is ${name}`)
}

greet()

// (...)

Imagine you are working on a really long legacy code base and have to implement a util function called greet. It takes in a parameter named name , combine it with a local variable salutation , and logs a string. If greet hadn't created its own execution scope, name identifier could have referred to some totally unrelated with too broad name declared a long time ago and the function call could have ended up logging something unexpected. However, thanks to the creation and removal of its own execution context,

  • variables refer to something we expect them to
  • and JavaScript engine can optimize memory by garbage collecting the variable salutation which is no longer referenced after greet().

References

Subscribe to go-kahlo-lee

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe