Note: This post is a translated version of an article originally published on my personal blog. You can read the original Korean post here.
Heads up: This post is written based on the ES5 specification. In ES6+, some details changed — such as where
ThisBindinglives, and the distinct roles ofVariableEnvironmentvsLexicalEnvironment. I'll cover the ES6 version in a follow-up post.
Why Should You Care About Execution Context?
If you've been writing JavaScript for a while, you've probably run into these puzzling moments:
- The same
thiskeyword points to completely different objects depending on the situation - A variable you never declared somehow doesn't throw an error
- You can freely access a variable that's defined outside your current scope
Sure, you can get by without knowing why these things happen. But eventually you hit a wall:
- Your code "just works" but you can't explain why
- You can't control what
thispoints to - Variables get shared in unintended ways, spawning bugs everywhere
Understanding execution context is the key to breaking through that wall.
What Is an Execution Context?
An execution context is an object that holds all the environmental information needed to execute a piece of code.
JavaScript might look like it runs line by line, but under the hood, the engine first collects and prepares environmental information before executing anything. That prepared environment is the execution context.
When an execution context is activated, it gathers three pieces of information:
- VariableEnvironment (VE) — a snapshot of the initial environment
- LexicalEnvironment (LE) — the live environment that reflects changes during execution
-
ThisBinding — what
thisrefers to in the current context
When Are Execution Contexts Created?
Execution contexts are created at two points:
- When JavaScript first runs → the Global Execution Context is created
- When a function is called → a Function Execution Context is created
The global execution context is born when the script starts and lives until the script ends.
A function execution context is created fresh every time a function is called, and it's discarded once the function finishes.
The Call Stack and Execution Contexts
Execution contexts are managed through the Call Stack.
function second() {
console.log('second');
}
function first() {
second();
}
first();
Here's how this plays out step by step:
- The global execution context is pushed onto the call stack
-
first()is called → its execution context is pushed on top -
second()is called → its execution context is pushed on top -
secondfinishes → its context is popped off -
firstfinishes → its context is popped off - The global context is removed when the script ends
The context at the top of the stack is the one currently running. When a new function is called, the current context pauses and the new one takes over.
VariableEnvironment vs. LexicalEnvironment
Both environments start out identical when an execution context is created. Over time, they diverge:
- LexicalEnvironment — updated in real time as code executes
- VariableEnvironment — frozen at the state it was in when the context was first created
LexicalEnvironment
The LexicalEnvironment has two components.
environmentRecord
This is where identifier information for the current execution context is stored — parameter names, function declarations, and variable names all live here.
Before executing any code, the JavaScript engine scans through the scope and builds up this environmentRecord. It collects all the variable names ahead of time.
This is what we call hoisting.
For a deeper look at hoisting, check out my hoisting post.
The global execution context works slightly differently: its environmentRecord is the global object (window in browsers, global in Node.js). This is exactly why variables declared with var at the top level are accessible via window.yourVariable.
outerEnvironmentReference
This is a reference to the LexicalEnvironment of the scope in which the current function was declared.
The word "declared" is key here. It doesn't matter where the function is called — only where it was written. This is what makes JavaScript lexically scoped, and it's also the core principle behind closures.
Thanks to outerEnvironmentReference, when the engine needs to look up a variable, it can start in the current scope and walk outward.
The Scope Chain
How Variables Are Looked Up
When the JavaScript engine needs to resolve a variable, it searches in this order:
- Look in the current context's
environmentRecord - If not found, follow
outerEnvironmentReferenceto the outer scope - Repeat this process all the way up to the global scope
- If still not found →
ReferenceError
This chain of scopes — where the engine climbs outward looking for a declaration — is called the scope chain.
Scope Chain in Action
var a = 'global';
function outer() {
var a = 'outer';
function inner() {
// No `a` in inner's scope
// → follows outerEnvironmentReference to outer's scope
console.log('inner >>> ', a);
}
console.log('outer >>> ', a);
inner();
}
outer();
console.log('global >>> ', a);
outer >>> outer
inner >>> outer
global >>> global
Even though inner never declares a, it finds outer's a through the scope chain.
Notice the last line: global >>> global. The var a = 'outer' inside outer is a completely separate variable from the global a. outer declared its own a in its own scope, so the global a stays untouched.
Scope Is One-Way: Inside-Out Only
An important property of the scope chain is that it's unidirectional — inner scopes can see outer scopes, but not the other way around.
function outer() {
var outerVar = 'outer';
}
console.log(outerVar); // ReferenceError: outerVar is not defined
Trying to access outerVar from outside the function throws an error because the global scope has no visibility into outer's internals.
Summary
- An execution context is an object holding the environmental info (VE, LE, ThisBinding) needed to run code.
- A new execution context is created every time a function is called; it gets pushed onto the call stack and popped off when done.
- The
environmentRecordin LexicalEnvironment stores identifier info for the current scope. - The
outerEnvironmentReferencepoints to the outer environment where the function was declared, not where it was called. - Variable lookup starts in the current scope and walks outward through
outerEnvironmentReference— this is the scope chain.
Closing Thoughts
Execution context is the foundation that ties together some of JavaScript's most important behaviors: hoisting, scope, and this binding all connect back to it.
In particular, the scope chain formed through outerEnvironmentReference is also the core mechanism behind closures — which I'll explore in a future post.
Thanks for reading!