Tuesday, September 2, 2025

Javascript Module 60

  Module 60 : Execution Context & Call Stack.

Prerequisites: Basic JavaScript syntax, functions, variables, console.log, and asynchronous primitives (setTimeout, Promises).

1. Big picture: what is an execution context?

An Execution Context (EC) is the environment in which JavaScript code is evaluated and executed. Think of it as a container that holds everything the engine needs to run a piece of code: the variables/local values, the scope chain (where to look up variables), the this binding, and links to outer lexical environments.


Every time code starts executing, an execution context is created and a stack frame representing it is pushed onto the call stack. When execution finishes, that frame is popped.


2. Types of execution contexts

Global Execution Context (GEC) — top-level; created when the script first runs. It creates the global object (e.g., window in browsers) and the global scope.

Function Execution Context — created whenever a function is invoked. Each call creates its own EC (even for the same function called multiple times).

Eval Execution Context — created by eval (rarely used and discouraged).

Module Execution Context — modules (import / export) have their own module EC with slightly different rules (they are strict mode by default).


3. Components of an execution context

Each EC contains:

Lexical Environment (or Environment Record): holds variable bindings for that context.

Variable Environment: similar to lexical environment but slightly different historically — for our purposes, the current environment record stores var, function declarations, and (for the lexical) let/const bindings once they are initialized.





This binding: value of this inside that context.

Outer reference: reference to the parent/outer lexical environment (used for scope chain lookup).


4. Two phases of execution (Creation & Execution)

When an execution context is created, the JavaScript engine (internally) does two conceptual phases:

Creation phase (sometimes called the hoisting phase):

Creates the Variable Object / Environment Record for the context.

Places function declarations into memory (the entire function object).

For var declarations: allocates the binding and sets it to undefined.

For let and const: creates the binding but leaves it in the Temporal Dead Zone (TDZ) until execution reaches its declaration.

Sets up the this binding for the context.

Execution phase:

The code runs line-by-line. Assignments initialize let/const bindings (leaving TDZ) and update vars.

Function calls create new function ECs which undergo the same two-phase lifecycle.

Important mental model: function declarations are hoisted (available during creation). var is hoisted but initialized to undefined. let and const exist but are not initialized until their line runs (TDZ).


5. Scope chain & closures

The scope chain is how the engine looks up identifiers: start in the current lexical environment, then follow the outer reference to the parent, and so on until the global environment.

A closure happens when an inner function references variables from its outer lexical environment. The inner function retains a reference to that environment even after the outer function has finished executing.


6. The Call Stack (stack frames)

The call stack (or execution stack) is a LIFO structure where each active execution context is represented as a stack frame.








When the program starts, the global context is pushed. Every function call pushes a new frame. When a function returns, its frame is popped.

Why it matters: Stack frames keep local variables and control flow. recursion or runaway calls can exhaust the stack (StackOverflow).


7. Event Loop, and the Call Stack 

The call stack is synchronous: JS runs one stack frame at a time. The Event Loop coordinates asynchronous events by moving callbacks from queues to the call stack when it's empty.

Macrotasks (task queue): setTimeout, setInterval, I/O, UI callbacks.

Microtasks (microtask queue): Promise handlers (.then) and queueMicrotask.

Order: after a stack empties, microtasks are processed to completion before the next macrotask.

Example important behavior:

console.log('start');

setTimeout(() => console.log('timeout'), 0);

Promise.resolve().then(() => console.log('promise'));

console.log('end');

// Output: start, end, promise, timeout


8. (with call-stack snapshots)

Example A — Simple nested calls

function a(){

  console.log('a start');

  b();

  console.log('a end');

}

function b(){

  console.log('b');

}

console.log('global start');

a();

console.log('global end');

Call stack timeline (conceptual):

Program starts — push Global EC.

console.log('global start') runs inside Global EC.

Call a() — push a() EC onto stack.

Inside a, console.log('a start') runs, then b() called — push b() EC.

b() logs 'b', returns — pop b() EC.

Continue a — log 'a end', return — pop a() EC.

console.log('global end'), finish — pop Global EC when program ends.

Example B — Hoisting and TDZ

console.log(foo); // ?

var foo = 'bar';


console.log(baz); // ?

let baz = 'qux';


sayHi(); // ?

function sayHi(){ console.log('hi'); }


sayBye(); // ?

var sayBye = function(){ console.log('bye'); }

Explain outputs:

console.log(foo) prints undefined because var foo is hoisted and initialized to undefined during the creation phase.



console.log(baz) throws ReferenceError: Cannot access 'baz' before initialization because let is in TDZ.

sayHi() works because function declarations are hoisted and available as full function objects during creation.

sayBye() throws TypeError because sayBye (declared with var) is undefined at the moment and you cannot call undefined as function.

Example C — Recursion (factorial) and stack frames

function fact(n){

  if(n <= 1) return 1;

  return n * fact(n - 1);

}

console.log(fact(4));

Call stack frames for fact(4):

fact(4) -> pushes frame for fact(4)

fact(3) -> pushes

fact(2) -> pushes

fact(1) -> pushes -> returns 1 -> pop fact(1)

fact(2) resumes, multiplies -> pop

fact(3) resumes -> pop

fact(4) resumes -> pop

Important: Each call holds its own n and return address. Deep recursion may cause RangeError: Maximum call stack size exceeded.

Example D — Closures and lifetime

function makeCounter(){

  let count = 0;

  return function(){

    count += 1;

    return count;

  }

}

const c = makeCounter();

console.log(c()); // 1

console.log(c()); // 2

Why count persists: The returned function's closure keeps a reference to the lexical environment that contains count. Even though makeCounter() returned and its frame was popped, the lexical environment remains alive because c keeps a reference.

Example E — Event loop & microtask vs macrotask

console.log('start');

setTimeout(()=> console.log('timeout'), 0);

Promise.resolve().then(()=> console.log('promise'));

console.log('end');

Output: start, end, promise, timeout. The promise .then handler is a microtask processed before the macrotask (the setTimeout callback).


9. exercises 

Exercise 1 — Trace the output & stack

console.log('1');

function foo(){

  console.log('2');

  bar();

  console.log('5');

}

function bar(){

  console.log('3');

  baz();

}

function baz(){

  console.log('4');

}

foo();

console.log('6');

Task: Write the order of console logs and show a short call stack trace at the point where baz() executes.

Solution: Output order: 1,2,3,4,5,6. Call stack at baz() execution: Global -> foo -> bar -> baz.


Exercise 2 — Hoisting trickiness

console.log(typeof funcA); // ?

console.log(typeof funcB); // ?


function funcA() {}

var funcB = function() {}

Solution:

typeof funcA -> 'function' (function declaration hoisted)

typeof funcB -> 'undefined' (var hoisted to undefined, typeof returns 'undefined')


Exercise 3 — TDZ failure












{

  console.log(a);

  let a = 5;

}

What's the result? Explain.

Solution: Throws ReferenceError because a is in TDZ until it's initialized.


Exercise 4 — Closure & memory (design)

Create a function createLogger() that returns a function which logs a message with an incrementing counter, but make sure it doesn't leak memory by accumulating unbounded data. Provide a safe implementation and explain why it doesn't leak.

Solution (one approach):

function createLogger(){

  let count = 0;

  return function(msg){

    count += 1;

    if(count > 1e6) count = 0; // prevents unbounded growth in certain scenarios

    console.log(`#${count}:`, msg);

  }

}

Why this is safe: The closure only keeps a single primitive (count). There's no accumulation of arrays/objects that would grow without bound.


Exercise 5 — Implement a simple call-stack visualizer (mini project)

Write a helper to trace function entry and exit so you can see call stack growth. Example: traceWrap(fn) should return a wrapped function that logs when it's entered and exited, with a depth-based indent.

Solution:

const _callStack = [];

function traceWrap(fn){

  return function traced(...args){

    _callStack.push(fn.name || '<anonymous>');

    console.log('>'.repeat(_callStack.length), 'enter', fn.name || '<anonymous>');

    try{

      return fn.apply(this, args);

    } finally {

      console.log('




No comments:

Post a Comment

Javascript Module 78

  Javascript   Module 78 If You want To Earn Certificate For My  All   Course Then Contact Me At My  Contact  Page   then I Will Take A Test...