Follow AiTechWorlds on LinkedIn for professional AI content!Follow Now →
20 minLesson 27 of 35
OOP & Advanced Concepts

Closures & Lexical Scope

Closures & Lexical Scope

Closures are one of the most powerful — and most misunderstood — features in JavaScript. A closure is what happens when a function remembers the variables from its surrounding scope, even after that scope has finished executing.

Lexical Scope

JavaScript uses lexical scope (also called static scope): a variable's availability is determined by where it's written in the code, not where it's called from.

const message = "outer";

function outer() {
    const message = "middle";
    
    function inner() {
        const message = "inner";
        console.log(message);  // "inner" — closest scope wins
    }
    inner();
    console.log(message);  // "middle"
}

outer();
console.log(message);  // "outer"

What Is a Closure?

A closure is created when a function is defined inside another function and the inner function references variables from the outer function:

function makeCounter() {
    let count = 0;  // this variable is "closed over"
    
    return function() {
        count++;          // still has access to count
        return count;
    };
}

const counter = makeCounter();
counter();  // 1
counter();  // 2
counter();  // 3

// count is NOT accessible from outside
console.log(count);  // ReferenceError

The inner function "closes over" count — it carries a reference to the variable, not a copy of its value at the time of creation.

Practical Closures

Private State

function createBankAccount(initialBalance) {
    let balance = initialBalance;  // private
    
    return {
        deposit(amount) {
            if (amount > 0) balance += amount;
            return balance;
        },
        withdraw(amount) {
            if (amount > balance) throw new Error("Insufficient funds");
            balance -= amount;
            return balance;
        },
        getBalance() {
            return balance;
        }
    };
}

const account = createBankAccount(1000);
account.deposit(500);    // 1500
account.getBalance();    // 1500
account.balance;         // undefined — truly private

Factory Functions

function makeAdder(x) {
    return (y) => x + y;  // closes over x
}

const add5 = makeAdder(5);
const add10 = makeAdder(10);

add5(3);   // 8
add10(3);  // 13
add5(7);   // 12

// Each closure has its own x

Memoization

function memoize(fn) {
    const cache = new Map();  // closed over by the returned function
    
    return function(...args) {
        const key = JSON.stringify(args);
        
        if (cache.has(key)) {
            return cache.get(key);
        }
        
        const result = fn.apply(this, args);
        cache.set(key, result);
        return result;
    };
}

const fib = memoize(function(n) {
    if (n <= 1) return n;
    return fib(n - 1) + fib(n - 2);
});

fib(40);  // fast — computed once, cached after

Event Handlers with Context

function setupButton(buttonId, message) {
    const btn = document.getElementById(buttonId);
    
    btn.addEventListener("click", () => {
        // 'message' is closed over — remembered when clicked
        alert(message);
    });
}

setupButton("btn1", "Hello!");
setupButton("btn2", "Goodbye!");
// Each button has its own closure with different message

The Classic Loop Closure Bug

// Bug: all callbacks share the same 'i'
for (var i = 0; i < 3; i++) {
    setTimeout(() => console.log(i), 100);
}
// Prints: 3, 3, 3 (not 0, 1, 2)
// Why: var is function-scoped, 'i' is the same variable, already = 3 by the time callbacks run

// Fix 1: Use let (block-scoped — creates a new 'i' per iteration)
for (let i = 0; i < 3; i++) {
    setTimeout(() => console.log(i), 100);
}
// Prints: 0, 1, 2 ✓

// Fix 2: Use an IIFE to capture the value (pre-ES6)
for (var i = 0; i < 3; i++) {
    (function(j) {
        setTimeout(() => console.log(j), 100);
    })(i);
}
// Prints: 0, 1, 2 ✓

Module Pattern

Closures enable the module pattern — encapsulating private state and exposing a public API:

const userStore = (function() {
    // Private
    const users = new Map();
    let nextId = 1;
    
    function validateUser(user) {
        if (!user.name) throw new Error("Name required");
        if (!user.email?.includes("@")) throw new Error("Valid email required");
    }
    
    // Public API
    return {
        add(user) {
            validateUser(user);
            const id = nextId++;
            users.set(id, { ...user, id });
            return id;
        },
        get(id) {
            return users.get(id);
        },
        list() {
            return [...users.values()];
        },
        count() {
            return users.size;
        }
    };
})();  // IIFE — immediately invoked

userStore.add({ name: "Alice", email: "alice@example.com" });
userStore.count();  // 1
userStore.users;    // undefined — private!

Closure and Memory

Closures keep referenced variables alive — which can cause memory leaks if you're not careful:

// Potential memory leak — large array stays in memory
function setup() {
    const bigData = new Array(1000000).fill("x");
    
    document.getElementById("btn").addEventListener("click", () => {
        // This closure captures bigData — it won't be garbage collected
        // as long as the event listener exists!
        console.log(bigData.length);
    });
}

// Fix: only close over what you need
function setup() {
    const bigData = new Array(1000000).fill("x");
    const dataLength = bigData.length;  // capture the small value
    
    document.getElementById("btn").addEventListener("click", () => {
        console.log(dataLength);  // only captures a number, not the array
    });
    // bigData can now be garbage collected after setup() returns
}

Next lesson: Error Handling with try/catch — defensive programming in JavaScript.

📱

Get this course's notes on Telegram!

Free cheat sheets, summaries & practice exercises

Get Notes Free →
!