Execution Context
Every time JavaScript runs code, it creates an execution context. Understanding execution contexts, the creation phase, and the scope chain explains hoisting, closures, and this binding.
What is an Execution Context?
An execution context is the environment in which JavaScript code is evaluated and executed. It contains:
- Variable Environment โ bindings for variables and functions
- Lexical Environment โ the same + outer reference (scope chain)
thisbinding โ whatthisrefers to in this context
There are three types:
- Global Execution Context (GEC) โ created when the script starts
- Function Execution Context (FEC) โ created for each function call
- Eval Execution Context โ created by
eval()(avoid this)
The Call Stack
Execution contexts are managed in a stack (LIFO):
function greet(name) { // FEC created
const message = "Hello, " + name;
return message;
}
function main() { // FEC created
const result = greet("Alice"); // push FEC(greet)
console.log(result); // pop FEC(greet)
}
main(); // push FEC(main)
// GEC is always at the bottom
Call stack states:
[GEC] โ script starts
[GEC, FEC(main)] โ main() called
[GEC, FEC(main), FEC(greet)] โ greet() called
[GEC, FEC(main)] โ greet() returns
[GEC] โ main() returns
Execution Context Phases
Every execution context goes through two phases:
Phase 1: Creation Phase
Before any code runs, JavaScript:
- Creates the
thisbinding - Creates the Lexical Environment (LexEnv) and Variable Environment (VarEnv)
- Allocates memory for variables and functions โ this is hoisting
What happens during creation:
- Function declarations โ stored with their full function body
varvariables โ stored asundefinedlet/constโ stored but uninitialized (Temporal Dead Zone)
// What the engine "prepares" before running this code:
console.log(myVar); // undefined โ var was hoisted and set to undefined
console.log(myFunc()); // "hello" โ function declaration fully hoisted
var myVar = "world";
function myFunc() {
return "hello";
}
Phase 2: Execution Phase
Code runs line by line. Variables get assigned their actual values.
The Lexical Environment
A lexical environment is a data structure that maps identifier names to their values. Each context has:
- Environment Record โ the actual bindings
- Outer Reference โ pointer to the parent lexical environment
This outer reference is what creates the scope chain:
const global = "I'm global";
function outer() {
const outerVar = "I'm outer";
function inner() {
const innerVar = "I'm inner";
// inner's outer reference โ outer's LexEnv
// outer's outer reference โ global LexEnv
console.log(innerVar, outerVar, global); // all accessible
}
inner();
}
Variable Environment vs Lexical Environment
In early spec, these were the same. Today the distinction is:
- Variable Environment โ handles
varbindings (function-scoped) - Lexical Environment โ handles
let,const, function declarations (block-scoped)
function example() {
var a = 1; // in VariableEnvironment
let b = 2; // in LexicalEnvironment
if (true) {
var c = 3; // also in VariableEnvironment (function-scoped)
let d = 4; // new LexicalEnvironment for the block
console.log(a, b, c, d); // all accessible
}
console.log(a, b, c); // accessible
// console.log(d); // ReferenceError โ d's LexEnv is the block above
}
Closures Through Execution Contexts
When a function is created, it stores a reference to its current lexical environment. This is the closure:
function makeAdder(x) {
// FEC(makeAdder) created
// LexEnv: { x: 5 } (after execution)
return function(y) {
// This inner function stores a reference to makeAdder's LexEnv
return x + y; // x is found in the outer LexEnv
};
}
const add5 = makeAdder(5);
// FEC(makeAdder) is gone from the call stack
// BUT: the inner function still holds a reference to LexEnv { x: 5 }
// The LexEnv stays in memory because add5 references it
console.log(add5(3)); // 8 โ x found via the closure reference
this Binding in Execution Contexts
The this binding is determined at context creation:
// Global context
// In browser: this = window
// In Node.js module: this = module.exports (or {} in CJS)
// In ES module: this = undefined
function regularFn() {
// this is determined by HOW this function is called
console.log(this);
}
const obj = {
method: regularFn // when called as obj.method(), this = obj
};
const arrow = () => {
// Arrow: no own this โ captured from creation context
console.log(this);
};
Scope Chain Resolution
When a variable is referenced, JavaScript walks the scope chain:
const x = "global x";
function level1() {
const x = "level1 x"; // shadows global x
function level2() {
// no x here
function level3() {
console.log(x); // found in level1's LexEnv: "level1 x"
}
level3();
}
level2();
}
level1();
Resolution order: current context โ parent โ parent's parent โ ... โ global โ ReferenceError.
Execution Context in Practice
Understanding execution contexts explains several behaviors:
// 1. Hoisting
function hoistingExample() {
console.log(a); // undefined (var hoisted)
var a = 5;
console.log(a); // 5
}
// 2. TDZ
function tdz() {
// console.log(b); // ReferenceError (let in TDZ)
let b = 5;
console.log(b); // 5
}
// 3. Closure preserving environment
function outer() {
let count = 0;
return () => count++;
}
const increment = outer();
increment(); increment(); increment();
// count is preserved in outer's LexEnv
// 4. this binding
const obj = {
value: 42,
getValue() {
// FEC created with this = obj (method call)
return this.value;
}
};
Key Takeaways
- Every execution context has a Variable Environment, Lexical Environment, and
thisbinding - Creation phase: variables and functions are "prepared" (hoisting happens here)
- Execution phase: code runs line by line, variables get their values
- The scope chain is built from outer references in lexical environments
- Closures work by storing a reference to the lexical environment at creation time
thisbinding is set during execution context creation
Ready to test your knowledge?
Take a quiz on what you just learned.