Cover image for Understanding JavaScript Execution Context: The Foundation of Interactivity

Alexandra

The most difficult thing in JavaScript is understanding how things are collaborating in order to provide interactivity.

The execution context is equivalent to a workspace. Whenever JS runs some code, it creates a workspace to contain everything it needs to do it successfully. That workspace has the variables, the functions, the this and information on where to continue working.

Creating contexts

There are two ways to create this workspace:

  1. When a program starts (Global execution context)
  2. Every time a function is called (Function execution context)
const name = "Alex";
function greet() {
    const message = "Hello";
    console.log(message);
}
greet();

Enter fullscreen mode Exit fullscreen mode

when this program starts JS creates the global context with the name and greet.Then the greet function is called, which creates a new execution context that includes the variable message. When the function greet() finishes its execution, this context is removed. When the program finishes the global context is also removed. A fair question here is: "removed from where"? The answer is: from the infamous call stack.

The call stack

These execution workspaces are stored in a stack. If you don't know what a stack is: it is a data structure with some specific properties. New elements are added at the top of the stack, elements are removed also from the top of the stack. The most famous comparison is to think of a stack as a stack of plate. You wouldn't remove a plate from the bottom of the stack. This property is called LIFO: Last in First out.

┌─────────────┐
│   greet()   │  ← Top (will be removed first)
├─────────────┤
│   Global    │  ← Bottom (removed last)
└─────────────┘

Enter fullscreen mode Exit fullscreen mode

When greet() finishes, it's "pop'ed" from the stack and only the global context remains (until the end of this program)

function first() {
    second();
}

function second() {
    third();
}

function third() {}

Enter fullscreen mode Exit fullscreen mode

Before any call:        During execution:       After all finish:
┌──────────────┐        ┌──────────────┐       ┌──────────────┐
│   Global     │        │   third()    │       │   Global     │
└──────────────┘        ├──────────────┤       └──────────────┘
                        │  second()    │
                        ├──────────────┤
                        │   first()    │
                        ├──────────────┤
                        │   Global     │
                        └──────────────┘

Enter fullscreen mode Exit fullscreen mode

As every functions finishes their executions, it's get popped from the stack. First the third finishes and it is removed, then the second, etc.

The two phases of the context

Every execution context has two phases. The 1st phase is the Creation: JS scans the code and prepares the memory, it identifies variables and functions.

  • Variables declared with var are initialised with undefined.
  • Functions defined with the function keyword are saved as a whole.
  • let and const variables are registered in an other place called Temporal Dead Zone.

The next phase is the execution: JS runs the code line-by-line. It assign values to variables and executes statements.

Why hoisting happens?

There is a word that scares every web developer: hoisting. This happens due to the creation phase. As JS scans the code it already knows about variables and functions before execution even begins. That process is called hoisting.

console.log(a); // Prints undefined, because JS initialized the a with undefined in the creation phase
var a = 5;

Enter fullscreen mode Exit fullscreen mode

Function declarations are fully hoisted, so you can call them even before they are written.

greet(); // This works! Prints "Hello"

function greet() {
    console.log("Hello");
}

Enter fullscreen mode Exit fullscreen mode

Note: function expressions are NOT hoisted. Function expressions are functions that are stored in a variable.

let and const against hoisting

In ES6 the keywords let and const were introduced. When you run the same code as before but using let this time..you get a reference error. what is happening?

console.log(a); // throws ReferenceError
let a = 5;

Enter fullscreen mode Exit fullscreen mode

Javascript still knows that a exists, but the difference is that now is not saved in the context. Instead, it's stored in an another place called Temporal dead zone (TDZ).

Understanding TDZ

The TDZ is a region in a block where a variable exists but cannot accessed (yet). TDZ starts from the beginning of the block until the variable is declared and initialized.

if (true) {
    // TDZ for 'a' starts here
    console.log(a); // ReferenceError - 'a' is in TDZ, not accessible

    let a = 5;      // TDZ ends here, 'a' is now initialized and accessible

    console.log(a); // 5 - now it works
}

Enter fullscreen mode Exit fullscreen mode

The Scope Chain: Looking Beyond the Current Context

When you reference a variable inside a function, JS doesnt only look in the current execution context. If the variable is not found in the current, it start looking on the parent, and then it's parent until it reaches the global context. This is called scope chain.

const global = "I'm a global var";

function outer() {
    const outerVar = "I'm in outer scope";

    function inner() {
        const innerVar = "I'm in inner scope";

        console.log(innerVar);  // ✅ Found in inner's context
        console.log(outerVar);  // ✅ Found in outer's context (via scope chain)
        console.log(global);    // ✅ Found in global context (via scope chain)
    }

    inner();
}

outer();

Enter fullscreen mode Exit fullscreen mode

summary

✅ JS creates an execution context before running code
✅ Every function call creates an execution context
✅ These contexts are managed using the call stack
✅ Each execution context has a creation and an execution phase.
✅ Hoisting happens in the creation phase due to the scan of the code.
const and let solved the initialization issues in JS by storing variables in TDZ
✅ The scope chain allows functions to access variables from their parent contexts (lexical scoping)