Learning Objectives

  • Understand execution contexts and the call stack
  • Learn about lexical environments
  • See how JavaScript engines handle closures
  • Understand memory management with closures

Execution Contexts

To understand how closures work, we need to understand execution contexts. When JavaScript code runs, it creates execution contexts that contain:

  • Variable Environment: Where variables are stored
  • Lexical Environment: References to outer scopes
  • this binding: The value of this

The Call Stack

JavaScript uses a call stack to manage execution contexts. Let's see it in action:

function first() {
    console.log("In first");
    second();
    console.log("Back in first");
}

function second() {
    console.log("In second");
    third();
    console.log("Back in second");
}

function third() {
    console.log("In third");
}

first();
Call Stack Evolution: 1. Initial: [Global Execution Context] 2. first() called: [first() Execution Context] [Global Execution Context] 3. second() called: [second() Execution Context] [first() Execution Context] [Global Execution Context] 4. third() called: [third() Execution Context] [second() Execution Context] [first() Execution Context] [Global Execution Context] 5. third() returns: [second() Execution Context] [first() Execution Context] [Global Execution Context] 6. second() returns: [first() Execution Context] [Global Execution Context] 7. first() returns: [Global Execution Context]

Lexical Environment

Each execution context has a lexical environment that consists of:

  • Environment Record: Stores variables and functions
  • Outer Reference: Link to the parent lexical environment
const globalVar = "global";

function outer() {
    const outerVar = "outer";
    
    function inner() {
        const innerVar = "inner";
        console.log(globalVar, outerVar, innerVar);
    }
    
    return inner;
}

const myFunc = outer();
myFunc();
Lexical Environment Chain: inner() Lexical Environment: ├─ Environment Record: { innerVar: "inner" } └─ Outer Reference ──→ outer() Lexical Environment ├─ Environment Record: { outerVar: "outer" } └─ Outer Reference ──→ Global Lexical Environment ├─ Environment Record: { globalVar: "global" } └─ Outer Reference: null

How Closures Are Created

When a function is created, it stores a reference to its lexical environment. This is how closures work:

function createCounter() {
    let count = 0;
    
    return function increment() {
        count++;
        return count;
    };
}

const counter = createCounter();

Here's what happens step by step:

  1. createCounter() is called, creating a new execution context
  2. A variable count is created in this context's environment
  3. The increment function is created and stores a reference to this environment
  4. createCounter() returns and its execution context is popped off the stack
  5. BUT the lexical environment is NOT garbage collected because increment still references it!
  6. When we call counter(), it accesses count through its stored reference

Memory Management

Closures keep their lexical environment alive in memory. This is powerful but requires understanding:

function createHeavyObject() {
    const largeArray = new Array(1000000).fill("data");
    
    return function() {
        // This closure keeps largeArray in memory!
        return largeArray.length;
    };
}

const getLength = createHeavyObject();
// largeArray is still in memory even though createHeavyObject finished

What Gets Captured?

JavaScript engines are smart - they only keep variables that are actually used by the closure:

function createClosure() {
    const used = "I'm used";
    const notUsed = "I'm not used";
    const alsoNotUsed = "Me neither";
    
    return function() {
        console.log(used); // Only 'used' is captured
    };
}

const myClosure = createClosure();
// Modern engines won't keep 'notUsed' and 'alsoNotUsed' in memory

Multiple Closures Sharing Environment

When multiple functions are created in the same scope, they share the same lexical environment:

function createCounter() {
    let count = 0;
    
    return {
        increment: function() {
            count++;
            return count;
        },
        decrement: function() {
            count--;
            return count;
        },
        getCount: function() {
            return count;
        }
    };
}

const counter = createCounter();
counter.increment(); // count = 1
counter.increment(); // count = 2
counter.decrement(); // count = 1
console.log(counter.getCount()); // 1

// All three methods share the same 'count' variable!

Visualizing Closure Memory

Memory After createCounter() Returns: Heap Memory: ┌─────────────────────────────────────┐ │ Lexical Environment (from outer) │ │ ┌─────────────────────────────────┐ │ │ │ count: 0 │ │ │ └─────────────────────────────────┘ │ │ ↑ ↑ ↑ │ │ │ │ │ │ │ ┌────┘ ┌────┘ ┌────┘ │ │ │ │ │ │ │ increment decrement getCount │ │ function function function │ └─────────────────────────────────────┘ Stack: ┌─────────────────────────────────────┐ │ counter = { increment, decrement, │ │ getCount } │ └─────────────────────────────────────┘

Performance Considerations

Closures are efficient, but keep these points in mind:

  • Each closure instance has its own environment (memory cost)
  • Closures prevent garbage collection of captured variables
  • Modern engines optimize closures heavily
  • The performance impact is usually negligible
// Creating many closures
const counters = [];
for (let i = 0; i < 1000; i++) {
    counters.push(createCounter());
}
// Each counter has its own lexical environment in memory

Key Takeaways

  • ✅ Functions store a reference to their lexical environment
  • ✅ Closures keep their environment alive even after the outer function returns
  • ✅ The call stack manages execution contexts
  • ✅ Modern engines only capture variables that are actually used
  • ✅ Multiple closures from the same scope share the same environment
  • ✅ Closures have minimal performance impact in most cases

Next Steps

You now understand the fundamentals of closures and how they work under the hood! In the next section, we'll explore practical patterns like data privacy, factory functions, and the module pattern.