This article was first published on the Open Replay Blog
With each new abstraction of the Javascript language, which can be in the form of a library or framework, Writing Javascript has become more declarative than imperative. Which is actually a good thing right, things now gets done faster and easier. However, no matter which framework or tool we use to write Javascript code, in the end, our code is still executed as vanilla Javascript by the Javascript engine.
This is why as a Javascript developer you should have a basic understanding of how Javascript works under the hood. Trust me this will make you a more confident and better Javascript developer.
INTRODUCTION
This article is for everyone who writes Javascript code, from the beginner to the senior Javascript 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 is, how the Execution Context is created and what it does.
TABLE OF CONTENTS
- WHAT IS THE EXECUTION CONTEXT
- TYPES OF EXECUTION CONTEXT
- WHAT IS THE EXECUTION STACK
- HOW THE EXECUTION CONTEXT IS CREATED
- UNDERSTANDING HOW CODE IS EXECUTED THROUGH THE EXECUTION CONTEXT
- THE REASON BEHIND HOISTING IN JAVASCRIPT
- CONCLUSION
WHAT IS THE EXECUTION CONTEXT
Imagine your code is placed inside a box whenever you write Javascript code. That box is the Execution context.
The Execution Context is a conceptual environment created by Javascript for code evaluation and execution.
TYPES OF EXECUTION CONTEXT
There are 3 types of Execution Context in javascript.
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 javascript.
It has two main functions. i. Create a global object. ii. 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.
- The
eval()
Execution Context: Due to the malicious nature of eval(), it is rarely used in javascript, So it won’t be talked about here. However, an execution context is created whenever it is used.
THE EXECUTION STACK (Call 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.
The 3 step process of the execution stack
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 it’s 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. - The
sayHi()
function is invoked and pushed on the stack.
STEP 3:
- The
sayHi()
function is popped off the stack. - The
greetings()
function is popped off the stack.
HOW THE EXECUTION CONTEXT IS CREATED
The execution context is created in two stages:
- Creation stage
- 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 main or primary Lexical Environment.
- Variable Environment: This is a type of Lexical Environment used specifically by the
var
VariableStatement within an Execution Context.
Conceptual Representation:
ExecutionContext = {
LexicalEnvironment = <ref. to LexicalEnvironment in memory>,
VariableEnvironment = <ref. to VariableEnvironment in memory>
}
Lexical Environment According to the official ES6 docs.
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 identifier variable 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
Environment Record According to the official ES6 docs: An Environment Record records the identifier bindings that are created within the scope of its associated lexical environment.
The Environment Record is simply the location where variable and function declarations are stored in a lexical environment. It is located in the memory heap of the Javascript 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.
Example
function add(a,b) {
let c = a + b;
}
add(10,20);
// argument object
Arguments: {0: 10, 1: 20, length: 2}
- 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).
Reference to an outer Lexical Environment 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 Javascript engine can look for variables in the outer Lexical Environment if they are not found in the inner Lexical Environment.
N.B— The Lexical Environment in the Global Execution Context has no reference to an outer Lexical Environment. It 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 that is referenced by the inner Lexical Environments of both function B
and function C
.
The this
Binding
The this
binding component is where the value of this 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, 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 company object. However, when companyAge()
is called, this refers to the global object.
Conceptual Representation of the Lexical Environment:
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 In this, the stage variables are assigned to their respective values and the code is executed.
UNDERSTANDING HOW CODE IS EXECUTED THROUGH THE EXECUTION CONTEXT
Now let’s understand how an actual JavaScript code goes through the two stages of the Execution Context.
Example
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 Javascript 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.
Global Code:
In the creation stage, the Javascript 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>
}
}
Function Code:
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 IN JAVASCRIPT
From the preceding section, we know that, during the creation stage when our code is scanned for variable and function declarations. 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 the reason 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 they are accessed before being declared. This is known as hoisting in Javascript, which is simply being able to access a variable before it is declared.
CONCLUSION
Now you know how the Javascript Engine Executes your code internally through the Execution Context. Learning concepts like this will make you confident as a developer and also make it easy to learn more fundamental concepts in Javascript like scopes and closures.
Summary In this article you learned:
- The Execution Context is an abstract environment created by Javascript 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.