Explaining JavaScript's Execution Context and Stack
With each new abstraction of the Javascript (JS) language, which can be in the form of a library or framework, Writing JS has become more declarative than imperative. This is a good thing; things get done faster and easier. However, no matter which framework or tool we use to write JS code, in the end, our code is still executed as vanilla JS by the JS engine. As a JS developer, you should understand how JS works under the hood.
This article is for everyone who writes JS code, from the beginner to the senior JS developer at Netflix deploying production-ready code today - you will still learn a thing or two! At the end of this article, You will know what the Execution Context and Stack are, how the Execution Context is created, and what it does.
What is the Execution Context?
Imagine your code is placed inside a box whenever you write JS code. That box is the Execution context, a conceptual environment created by JS for code evaluation and execution.
There are three types of Execution Context in JS.
- The Global Execution Context (GEC): This is the primary or base Execution Context. It is the highest level of abstraction for an execution context in JS. It has two main functions: (1) Create a global object, and (2) Attach the
this
value to the global object - The Function Execution Context (FEC): An execution context is created for every function invocation. It is not created when the function is declared, only when called or invoked. When a function is called, it is executed through the execution stack (which we’ll discuss below).
- The
eval()
Execution Context: Due to the malicious nature of eval(), it is rarely used in JS so we won’t discuss it. However, an execution context is created whenever it is used.
The Call (Execution) Stack
A stack is a data structure that follows the Last in First Out (LIFO) principle. However, the execution stack is a stack that keeps track of all the execution context created during code execution.
- STEP 1: The global execution context is initially added(pushed) on the execution stack by default, in the form of the global() object.
- STEP 2: A Function execution context is added to the stack when a function is invoked or called.
- STEP 3: The Invoked function is executed and removed (popped) from the stack, along with its execution context.
Example:
function greeting() {
sayHi();
}
function sayHi() {
return "Hi!";
}
// Invoke the `greeting` function
greeting();
- STEP 1: The GEC is created and pushed on the execution stack as the
global()
object. - STEP 2: The
greeting()
function is invoked and pushed on the stack. - STEP 2: The
sayHi()
function is invoked and pushed on the stack. - STEP 3: The
sayHi()
function is popped off the stack. - STEP 3: The
greetings()
function is popped off the stack.
How the execution context is created.
The execution context is created in two stages: 1. Creation stage, and 2. Execution stage.
CREATION STAGE
A lexical environment is created in the creation stage of the execution context. It is of two types:
- Lexical Environment: This is the primary Lexical Environment.
- Variable Environment: This is a Lexical Environment used explicitly by the
var
VariableStatement within an Execution Context.
Conceptually, it is:
ExecutionContext = {
LexicalEnvironment = <ref. to LexicalEnvironment in memory>,
VariableEnvironment = <ref. to VariableEnvironment in memory>
}
According to the official ES6 documentation, A Lexical Environment is a specification type used to define the association of Identifiers to specific variables and functions based upon the lexical nesting structure of ECMAScript code. A Lexical Environment consists of an Environment Record and a possibly null reference to an outer Lexical Environment.
The lexical environment is a standard or specification type. It defines the identifier that represents a specific variable or function, based on its lexical nesting structure or scope. The lexical environment is simply a structure that contains variable identifier mapping.
Example:
let x = 10;
let y = 20;
function greeting(){
console.log('Hello');
}
The lexical environment for the preceding code will look like this:
lexicalEnvironment = {
x: 10,
y: 20,
greeting: <ref. to greeting function>
}
The Lexical Environment is made up of three components:
- Environment Record
- Reference to an outer environment
- The
this
binding
According to the official ES6 docs: An Environment Record records the identifier bindings created within the scope of its associated lexical environment. The Environment Record is the location where variable and function declarations are stored in a lexical environment, and it is located in the memory heap of the JS engine.
The Environment Record is of two types:
- Declarative Environment Record: This is mainly used by a Lexical Environment created in the Function Execution Context. It records (store) function declarations and variable declarations. The Declarative Environment Record also stores an
arguments
object, which contains the length(number) of argument(s) passed to a function, and a map of the argument(s) and its index. - Object Environment Record: This is mainly used by a Lexical Environment created in the Global Execution Context. It stores function declarations, variable declarations, and a global binding object(window object in browsers).
Example
function add(a,b) {
let c = a + b;
}
add(10,20);
// argument object
Arguments: {0: 10, 1: 20, length: 2}
Every (inner) Lexical Environment reference an outer Lexical Environment. The outer Lexical Environment logically surrounds the inner Lexical Environment that it is referenced by. The JS engine can look for variables in the outer Lexical Environment if they are not found in the inner Lexical Environment. The Lexical Environment in the Global Execution Context does not reference an outer Lexical Environment but is referenced to null
.
Example
function A () {
function B () {
// some code
}
function C () {
// some code
}
// some code
}
In the preceding code, function A
contains two nested functions function B
, and function C
. Since function B
and function C
are nested in function A
. Therefore, the Lexical Environment of function A
will be the outer Lexical Environment referenced by the inner Lexical Environments of both function B
and function C
.
The this
Binding
The this
binding component is where its value is set. The value of this
is set to the global object (i.e., window object in browsers) in the Global Execution Context. However, in the Function Execution Context, the this
value depends on how the function is called. If the function is a method in an object, this
value of the function is set to that object when called by the object reference. Otherwise, it is set to the global object.
Example
const company = {
name: 'facebook',
yearFound: 2004,
getAge: function() {
const date = new Date();
let currentYear = date.getFullYear();
console.log(currentYear - this.yearFound);
}
}
company.getAge();
const companyAge = company.getAge;
companyAge();
In the preceding code, when the company.getAge()
method is called, this
will refer to the company object. However, when companyAge()
is called, this refers to the global object.
Conceptually:
GlobalExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Object",
// Identifier bindings go here
}
outerEnv: <null>,
this: <global object>
}
}
FunctionExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// Identifier bindings go here
}
outerEnv: <Global or outer function environment reference>,
this: <depends on how function is called>
}
}
Variable Environment
The Variable Environment has been explained earlier in this section, as a type of Lexical Environment used specifically by the var
VariableStatement within an Execution Context. This means that the Environment Record of the VariableEnvironment only records bindings created by the var
VariableStatement. Whereas the Environment Record of the LexicalEnvironment records both variable (let
and const
) bindings and function declarations.
EXECUTION STAGE The stage variables are assigned to their respective values, and the code is executed.
Open Source Session Replay
OpenReplay is an open-source, session replay suite that lets you see what users do on your web app, helping you troubleshoot issues faster. OpenReplay is self-hosted for full control over your data.
Start enjoying your debugging experience - start using OpenReplay for free.
How code is executed
Now let’s understand how an actual JS code goes through the two stages of the Execution Context.
let a = 10;
const b = 20;
var c;
function add(e, f) {
var d = 30;
return e + f + d;
}
c = add(40, 50);
This is how the JS engine will execute the preceding code in the two stages of the Execution Context.
Creation stage
Here our code is being scanned for variable and function declarations. In the creation stage, the JS Engine creates a Global Execution Context to Execute the global code, which should look like this.
GlobalExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Object",
// Identifier bindings go here
a: < uninitialized >,
b: < uninitialized >,
add: < func >
}
outerEnv: <null>,
ThisBinding: <Global Object>
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Object",
// Identifier bindings go here
c: undefined,
}
outerEnv: <null>,
ThisBinding: <Global Object>
}
}
When the add(40, 50) function is called, a new Function Execution Context is created to execute the function code. The Function Execution Execution Context will look like this in the creation stage.
FunctionExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// Identifier bindings go here
Arguments: {0: 40, 1: 50, length: 2},
},
outerEnv: <GlobalLexicalEnvironment>,
ThisBinding: <Global Object or undefined>,
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// Identifier bindings go here
d: undefined
},
outerEnv: <GlobalLexicalEnvironment>,
ThisBinding: <Global Object or undefined>
}
}
Execution Stage
Here values are assigned to variables.
Global Code: In the Execution Stage, when variable assignments are done. This is what the Global Execution Context will look like.
GlobalExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Object",
// Identifier bindings go here
a: 10,
b: 20,
add: < func >
}
outerEnv: <null>,
ThisBinding: <Global Object>
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Object",
// Identifier bindings go here
d: undefined,
}
outerEnv: <null>,
ThisBinding: <Global Object>
}
}
Function Code: After the Global Execution Context has gone through the execution stage, assignments to variables are done. This also includes variables inside every function declaration in the Global Execution Context. The Function Execution Context will look like this during the execution stage.
FunctionExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// Identifier bindings go here
Arguments: {0: 40, 1: 50, length: 2},
},
outerEnv: <GlobalLexicalEnvironment>,
ThisBinding: <Global Object or undefined>,
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// Identifier bindings go here
d: 30
},
outerEnv: <GlobalLexicalEnvironment>,
ThisBinding: <Global Object or undefined>
}
}
The program finishes after the add()
function is executed and returns a value that is stored inside c
.
The reason behind hoisting
From the preceding section, we know that our code is scanned for variable and function declarations during the creation stage. The function declaration is stored completely in memory, while variables are initially set to undefined
(for var declaration) or remain uninitialized (for let
and const
declarations).
This is why variables defined with var
can be accessed before they are declared, though you will get a value of undefined
. However, this is not the case for variables defined with let and const, which will throw a reference error
when accessed before being declared. This is known as hoisting in JS, which is simply being able to access a variable before it is declared.
Conclusion
In this article, you learned:
- The Execution Context is an abstract environment created by JS to execute code.
- The Execution Stack is used to execute functions and keep track of all the Execution Context created.
- The Execution context is created in two stages, the creation and the execution stage.
- The Lexical Environment is a core part of the creation stage.
- Variable assignments are done in the execution stage.
- Hoisting is being able to access a variable before it is declared.
Now you know how the JS Engine Executes your code internally through the Execution Context. Learning concepts like this will make you confident as a developer and make it easy to learn more fundamental concepts in JS, like scopes and closures.