Advanced Closures

Reviewed & published by Brayan K

A closure is a function bundled together with the variables from the scope where it was created, letting it remember and keep accessing those variables even after that outer function has finished running.

Part of the free JavaScript course at LearnCodingFast β€” hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

Advanced Scope, Closures & Lexical Environments

Master JavaScript's scope system, closures, and lexical environments to become a top 1% developer.

What You'll Learn in This Lesson

πŸ’‘ Running Code Locally: While this online editor runs real JavaScript, some advanced examples may have limitations. For the best experience:

Understanding JavaScript's Scope System

JavaScript's scope system is one of the most misunderstood parts of the entire language, especially once you combine closures, async execution, event handlers, and nested functions. Understanding lexical environments is the real key to mastering how JavaScript stores variables, how functions remember old values, how async code captures state, and how real-world bugs like stale data, memory leaks, or broken loops happen.

This lesson will turn you into the top 1% of JS developers by teaching how variables live, die, and evolve inside the engine.

πŸ”₯ Lexical Environment β€” The Root of All JavaScript Behaviour

Every time JavaScript runs a block, function, or script, it creates a Lexical Environment. It contains three things:

Lexical Environment Example

let x = 10;

function show() {
    let y = 20;
    console.log(x + y);
}

show();

This is lexical scoping β€” meaning variables are resolved based on where code is written, not where it's called.

πŸ”₯ Block Scope vs Function Scope β€” Why This Matters in Real Apps

Function Scope

function test() {
    var x = 1;
}
console.log(x); // error

Block Scope

Created by {} combined with let or const:

{
    let a = 5;
}
console.log(a); // error

Why does this matter?

Because incorrect scoping is one of the main causes of:

πŸ”₯ Closure β€” A Function That Remembers the Past

A closure is created whenever a function captures variables from an outer lexical environment even after that environment is gone.

Basic Closure Example

function counter() {
    let c = 0;
    return function() {
        c++;
        return c;
    };
}

const count = counter();
console.log(count()); // 1
console.log(count()); // 2

Why does it work?

Because the inner function remembers the variable c from the lexical environment of counter() even though counter() already finished executing.

Closures are not "magic" β€” they are simply preserved lexical environments.

πŸ”₯ Closures Used in Real Production-Level Code

1. Private State (common in frameworks)

function createStore() {
    let state = 0;

    return {
        get: () => state,
        set: (v) => state = v
    };
}

const store = createStore();
store.set(100);
console.log(store.get()); // 100

2. Debouncing / Throttling (used in UI + API calls)

function debounce(fn, delay) {
    let timeout;

    return function(...args) {
        clearTimeout(timeout);
        timeout = setTimeout(() => fn(...args), delay);
    };
}

This uses closure to remember timeout.

3. Custom Iterators

function createIdGenerator() {
    let id = 0;
    return () => ++id;
}

A closure stores the last ID forever.

πŸ”₯ The Scope Chain β€” The Path JavaScript Uses to Find Variables

When JS tries to access a variable:

Scope Chain Example

let a = 1;

function first() {
    let b = 2;

    function second() {
        let c = 3;
        console.log(a, b, c);
    }

    second();
}
first();

Scope chain order for second():

If b doesn't exist in second(), JavaScript climbs up.

This is exactly how closures work.

πŸ”₯ Temporal Dead Zone β€” Why let/const Behave Differently

The TDZ is the period between:

console.log(a); // ❌ ReferenceError
let a = 10;

Because let and const exist in lexical environment but cannot be used before initialization.

var does not have a TDZ:

console.log(a); // undefined
var a = 10;

This is why modern JS avoids var.

πŸ”₯ Real Bug From TDZ: Shadowed Variables

let x = 5;

function test() {
    console.log(x); // ❌ ReferenceError
    let x = 10;
}

Even though global x exists, the block-scoped x "shadows" it, causing TDZ.

πŸ”₯ Lexical Closures + Loops β€” Famous Interview Bug

for (var i = 0; i < 3; i++) {
    setTimeout(() => console.log(i), 0);
}

Because all async callbacks close over the SAME i.

for (let i = 0; i < 3; i++) {
    setTimeout(() => console.log(i), 0);
}

let creates a NEW lexical environment for each iteration.

πŸ”₯ Closures in Async Functions β€” Why This Confuses Beginners

function delayedLog() {
    for (let i = 1; i <= 3; i++) {
        setTimeout(() => console.log(i), i * 1000);
    }
}
delayedLog();

Each iteration gets its own lexical environment.

function delayedLog() {
    for (var i = 1; i <= 3; i++) {
        setTimeout(() => console.log(i), i * 1000);
    }
}

Because the closure captures one shared variable.

πŸ”₯ Massive Real-World Application: React Hooks Use Closures Internally

React's useState uses closures to maintain values across renders:

function useState(initial) {
    let value = initial;
    function get() { return value; }
    function set(v) { value = v; }
    return [get, set];
}

Every state value lives inside its own lexical environment.

πŸ”₯ Memory Leaks From Closures β€” What Causes Them

Closures hold onto variables even when not needed anymore.

function heavy() {
    const big = new Array(10000000);

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

const fn = heavy();

big stays in memory because the closure keeps the environment alive.

Real apps leak memory when:

Understanding lexical environments lets you avoid costly leaks.

πŸ”₯ Hoisting + Scope + Closures β€” The Hidden Interaction

Hoisting isn't just about "moving variables to the top." It interacts directly with lexical environments.

Hoisting Example

console.log(a); // ❌ TDZ Error
console.log(b); // undefined
console.log(sum()); // works

let a = 10;
var b = 20;
function sum() { return a + b; }

Understanding this behaviour makes closure debugging 100Γ— easier.

πŸ”₯ Lexical Environment Snapshots β€” The Rule Behind Every Closure

A closure captures the lexical state when the function is created, not when it is later executed.

Snapshot Example

function createMessage(msg) {
    return function() {
        console.log("Message:", msg);
    };
}

const hi = createMessage("Hello");
hi(); // "Hello"

Even if msg is changed later:

let hi2 = createMessage("A");
hi2(); // A
hi2 = createMessage("B");
hi2(); // B

Each closure keeps its own snapshot of msg. This is why closures feel like they store memories.

πŸ”₯ Closures Inside Objects β€” Not the Same as "this"

Closures capture variables in lexical scope. this depends on how a function is called.

function createUser(name) {
    return {
        say() { console.log(name); }
    };
}

const u = createUser("Boopie");
u.say(); // "Boopie"
function User(name) {
    this.name = name;
    this.say = function() {
        console.log(this.name);
    };
}

This one uses this, not closure. Learning the difference is critical for: React classes, Node.js services, Event handlers, Constructor patterns.

πŸ”₯ Deep Closure Pattern β€” Function Factory with Internal State

A powerful closure concept used in real apps:

function createBankAccount() {
    let balance = 0;

    return {
        deposit(amount) {
            balance += amount;
        },
        withdraw(amount) {
            balance -= amount;
        },
        getBalance() {
            return balance;
        }
    };
}

const acc = createBankAccount();
acc.deposit(100);
console.log(acc.getBalance()); // 100

This is true private data β€” not accessible from the outside. Closures enable data encapsulation without classes.

πŸ”₯ Recursion + Closures β€” Understanding How Scopes Stack

Recursive functions create new execution contexts every call. But shared variables still live in outer lexical environments:

function adder() {
    let total = 0;

    function add(n) {
        if (n <= 0) return total;
        total += n;
        return add(n - 1);
    }

    return add;
}

const sum = adder();
console.log(sum(5)); // 15

Each recursive call creates a new function context but the closure stores the shared state.

πŸ”₯ Asynchronous Closures β€” The Hardest Concept for Beginners

Async functions create closures too.

function loadUser() {
    let name = "loading...";

    fetch("/api/user").then(res => res.json()).then(data => {
        name = data.name;
    });

    return function() {
        console.log("User:", name);
    };
}

const user = loadUser();
setTimeout(() => user(), 2000);

Even after loadUser() finishes, the returned function keeps the lexical environment alive.

This is exactly how:

πŸ”₯ Closure Debugging Tip β€” Logging the Environment Too Early Is Misleading

Example that confuses learners:

function example() {
    let x = 0;

    setTimeout(() => console.log(x), 1000);

    x = 10;
}
example();

Closures keep references, not copies.

πŸ”₯ How Closures Cause Memory Leaks in Real Apps

Closures keep environment records alive. If a large object is captured inside a closure that lives forever, memory leaks.

function attachHeavyEvent() {
    let big = new Array(10000000).fill("data");

    document.addEventListener("click", function() {
        console.log("Hi");
    });
}

Because the closure references the lexical environment, big never gets garbage-collected.

function attachSafeEvent() {
    document.addEventListener("click", () => {
        console.log("Hi");
    });
}

Now nothing large is captured.

πŸ”₯ Closures + Module Pattern β€” How Real Libraries Secure Their Internals

Before ES modules existed, developers used closures to encapsulate logic:

const CounterModule = (function () {
    let value = 0;

    function increment() { value++; }
    function decrement() { value--; }
    function get() { return value; }

    return { increment, decrement, get };
})();

This is still used in:

Modules exist because closures made them possible to begin with.

πŸ”₯ Memoization β€” Closures for High-Performance Functions

Memoization caches results using closures and can reduce expensive computation costs dramatically.

function memoize(fn) {
    const cache = {};

    return function (n) {
        if (cache[n]) return cache[n];
        const result = fn(n);
        cache[n] = result;
        return result;
    };
}

const slowFib = n => n <= 1 ? n : slowFib(n-1) + slowFib(n-2);
const fastFib = memoize(slowFib);

console.log(fastFib(35));

The closure keeps cache alive and invisible to the outside world.

πŸ”₯ Throttling & Debouncing β€” Closures Power Every Search Bar & UI Input

Debounce:

function debounce(cb, delay) {
    let timer;
    return function (...args) {
        clearTimeout(timer);
        timer = setTimeout(() => cb(...args), delay);
    };
}

Throttle:

function throttle(cb, limit) {
    let waiting = false;
    return function (...args) {
        if (!waiting) {
            cb(...args);
            waiting = true;
            setTimeout(() => waiting = false, limit);
        }
    };
}

All of them work because closures store timers & state.

πŸ”₯ Event Emitters β€” Node.js Relies on Closure-Based State

A basic EventEmitter implementation:

function createEmitter() {
    const events = {};

    return {
        on(event, handler) {
            if (!events[event]) events[event] = [];
            events[event].push(handler);
        },
        emit(event, data) {
            (events[event] || []).forEach(fn => fn(data));
        }
    };
}

const bus = createEmitter();
bus.on("hello", msg => console.log(msg));
bus.emit("hello", "Hi from closure!");

The emitter's internal event registry lives inside a closure.

πŸ”₯ Closure + Currying β€” Advanced Functional Programming

Currying relies entirely on closures.

function multiply(a) {
    return function(b) {
        return function(c) {
            return a * b * c;
        };
    };
}

console.log(multiply(2)(3)(4)); // 24

Each nested function receives its own lexical environment.

πŸ”₯ Factory Functions β€” Alternative to Classes Using Closures

function createUser(name, age) {
    let score = 0;

    return {
        name,
        age,
        addScore() { score++; },
        getScore() { return score; }
    };
}

const u = createUser("Boopie", 16);
// 🎯 YOUR TURN β€” replace each ___ using the hint beside it.

function makeCounter(start) {
  // 1) This variable outlives the call, because the returned functions hold it.
  ___ count = start;                     // πŸ‘‰ replace ___ with let
  return {
    increment() { count += 1; return count; },
    peek() { return count; },
  };
}

// 2) Each call makes its OWN count. They do not share one.
const a = makeCounter(10);
const b = ___(100);                      // πŸ‘‰ replace ___ with makeCounter
console.log(a.increment(), a.increment(), b.increment());
console.log("a sees", a.peek(), "and b sees", b.peek());

const handlers = [];
// 3) var is function-scoped, so all three closures share ONE i β€” which is 3
//    by the time any of them runs.
for (___ i = 0; i < 3; i++) handlers.push(() => i);   // πŸ‘‰ replace ___ with var
console.log("var captured:", handlers.map((h) => h()));

const better = [];
// 4) let is block-scoped: a fresh binding per turn of the loop.
for (___ j = 0; j < 3; j++) better.push(() => j);     // πŸ‘‰ replace ___ with let
console.log("let captured:", better.map((h) => h()));

function once(fn) {
  let called = false;
  let result;
  return (...args) => {
    // 5) The remembered flag is what makes this run one time only.
    if (!___) { called = true; result = fn(...args); }   // πŸ‘‰ replace ___ with called
    return result;
  };
}
const init = once((n) => { console.log("running init"); return n * 2; });
console.log(init(21), init(99));

// βœ… Expected output:
// 11 12 101
// a sees 12 and b sees 101
// var captured: [ 3, 3, 3 ]
// let captured: [ 0, 1, 2 ]
// running init
// 42 42

Factories are used everywhere:

πŸ”₯ Closure Performance β€” When Overusing Closures Slows You Down

πŸ”₯ Closure Debugging Checklist

When closures behave unexpectedly, check:

Key Takeaways

Practice quiz

What is a closure?

  • A function with no arguments
  • A way to close a file handle
  • A function bundled with variables from the scope where it was created
  • A block that ends with a return

Answer: A function bundled with variables from the scope where it was created. A closure is a function plus the variables from its creation scope, which it keeps accessing even after the outer function finishes.

What does a Lexical Environment's Outer Environment Reference point to?

  • The next scope to search when a variable isn't found locally
  • The global object only
  • The function's return value
  • The call stack

Answer: The next scope to search when a variable isn't found locally. The outer environment reference is where JavaScript looks next if a variable isn't found in the current environment record.

What does the counter closure log: const count = counter(); count(); count();

  • 0 then 1
  • 1 then 1
  • undefined then undefined
  • 1 then 2

Answer: 1 then 2. The inner function remembers c across calls, so it returns 1 then 2.

With var in a loop, what do three setTimeout callbacks print for i (loop 0..2)?

  • 0 1 2
  • 3 3 3
  • 0 0 0
  • 2 2 2

Answer: 3 3 3. All callbacks close over the same shared var i, which is 3 by the time they run.

Why does switching the loop to let fix the bug, printing 0 1 2?

  • let creates a new lexical environment per iteration
  • let runs faster
  • let delays the callbacks
  • let copies the value into the timeout

Answer: let creates a new lexical environment per iteration. let creates a fresh binding for each iteration, so each callback closes over its own i.

What is the Temporal Dead Zone (TDZ)?

  • A region where var is undefined
  • A delay before async code runs
  • The time before a let/const variable is initialized, where using it throws
  • The garbage collection pause

Answer: The time before a let/const variable is initialized, where using it throws. The TDZ is the span from the start of scope until a let/const is declared, during which accessing it throws a ReferenceError.

Does var have a Temporal Dead Zone?

  • Yes, same as let
  • No β€” var is hoisted and initialized to undefined
  • Only inside functions
  • Only in strict mode

Answer: No β€” var is hoisted and initialized to undefined. var has no TDZ; it is hoisted and initialized to undefined, so reading it early gives undefined, not an error.

In memoize(fn), where does the cache live?

  • On the global object
  • In localStorage
  • On the fn parameter
  • Inside the closure, invisible to the outside

Answer: Inside the closure, invisible to the outside. The cache is kept alive by the returned function's closure and is private to it.

A closure captures variables by...

  • value (a copy at creation time)
  • reference (it sees later mutations)
  • deep clone
  • name only

Answer: reference (it sees later mutations). Closures keep references, not copies; setting x = 10 after scheduling a timeout makes it log 10, not 0.

What does multiply(2)(3)(4) return for the curried multiply?

  • 9
  • 234
  • 24
  • An error

Answer: 24. Each nested function closes over its argument, so 2 * 3 * 4 is 24.

Continue this course