JavaScript Closures

Functions That Remember

2026-02-01

Explanation

What is a Closure?

A closure is a function that has access to variables from its outer (enclosing) scope, even after the outer function has returned. Closures "close over" their environment.

Key Concepts

  • Lexical Scoping: Functions can access variables from where they're defined
  • Persistence: Outer variables persist in memory
  • Privacy: Create truly private variables
  • Factory Pattern: Generate customized functions

Demonstration

Example 1: Basic Closures

// Simple closure
function outer() {
    const message = 'Hello';  // Outer variable

    function inner() {
        console.log(message);  // Accesses outer variable
    }

    return inner;
}

const myFunc = outer();  // outer() returns inner
myFunc();  // 'Hello' - still has access to 'message'!

// The closure in action
function createCounter() {
    let count = 0;  // Private variable

    return function() {
        count++;
        return count;
    };
}

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

// Each call creates a new closure
const counter2 = createCounter();
console.log(counter2());  // 1 (independent)

// Closures preserve references, not values
function createFunctions() {
    const funcs = [];

    for (var i = 0; i < 3; i++) {
        funcs.push(function() {
            return i;  // All reference the same 'i'
        });
    }

    return funcs;
}

const funcs = createFunctions();
console.log(funcs[0]());  // 3 (not 0!)
console.log(funcs[1]());  // 3 (not 1!)
console.log(funcs[2]());  // 3 (not 2!)

// Fix with let (block scope)
function createFunctionsFixed() {
    const funcs = [];

    for (let i = 0; i < 3; i++) {
        funcs.push(function() {
            return i;  // Each has its own 'i'
        });
    }

    return funcs;
}

Example 2: Private Variables

// Module pattern with closures
function createBankAccount(initialBalance) {
    let balance = initialBalance;  // Private!
    const transactions = [];       // Private!

    return {
        deposit(amount) {
            if (amount <= 0) throw new Error('Invalid amount');
            balance += amount;
            transactions.push({ type: 'deposit', amount, date: new Date() });
            return balance;
        },

        withdraw(amount) {
            if (amount <= 0) throw new Error('Invalid amount');
            if (amount > balance) throw new Error('Insufficient funds');
            balance -= amount;
            transactions.push({ type: 'withdrawal', amount, date: new Date() });
            return balance;
        },

        getBalance() {
            return balance;  // Read-only access
        },

        getTransactions() {
            return [...transactions];  // Return copy
        }
    };
}

const account = createBankAccount(1000);
account.deposit(500);
account.withdraw(200);
console.log(account.getBalance());  // 1300

// Cannot access directly
console.log(account.balance);       // undefined
console.log(account.transactions);  // undefined

// Private class fields (modern alternative)
class BankAccount {
    #balance;        // Private field
    #transactions = [];

    constructor(initialBalance) {
        this.#balance = initialBalance;
    }

    deposit(amount) {
        this.#balance += amount;
        this.#transactions.push({ type: 'deposit', amount });
        return this.#balance;
    }

    get balance() {
        return this.#balance;
    }
}

Example 3: Function Factories

// Create customized functions
function multiply(factor) {
    return function(number) {
        return number * factor;
    };
}

const double = multiply(2);
const triple = multiply(3);
const quadruple = multiply(4);

console.log(double(5));     // 10
console.log(triple(5));     // 15
console.log(quadruple(5));  // 20

// Tax calculator factory
function createTaxCalculator(taxRate) {
    return function(amount) {
        const tax = amount * taxRate;
        return {
            amount,
            tax,
            total: amount + tax
        };
    };
}

const calculateNYTax = createTaxCalculator(0.08875);
const calculateCATax = createTaxCalculator(0.0725);

console.log(calculateNYTax(100));  // { amount: 100, tax: 8.875, total: 108.875 }
console.log(calculateCATax(100));  // { amount: 100, tax: 7.25, total: 107.25 }

// Logger factory
function createLogger(prefix) {
    return {
        log: (msg) => console.log(`[${prefix}] ${msg}`),
        error: (msg) => console.error(`[${prefix}] ERROR: ${msg}`),
        warn: (msg) => console.warn(`[${prefix}] WARNING: ${msg}`)
    };
}

const apiLogger = createLogger('API');
const dbLogger = createLogger('Database');

apiLogger.log('Request received');
dbLogger.error('Connection failed');

Example 4: Memoization

// Basic memoization
function memoize(fn) {
    const cache = {};  // Closure over cache

    return function(...args) {
        const key = JSON.stringify(args);

        if (key in cache) {
            console.log('Cache hit');
            return cache[key];
        }

        console.log('Computing...');
        const result = fn.apply(this, args);
        cache[key] = result;
        return result;
    };
}

// Expensive calculation
function fibonacci(n) {
    if (n <= 1) return n;
    return fibonacci(n - 1) + fibonacci(n - 2);
}

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

console.log(memoizedFib(40));  // Fast!

// LRU Memoization with size limit
function memoizeWithLimit(fn, maxSize = 100) {
    const cache = new Map();

    return function(...args) {
        const key = JSON.stringify(args);

        if (cache.has(key)) {
            // Move to end (most recently used)
            const value = cache.get(key);
            cache.delete(key);
            cache.set(key, value);
            return value;
        }

        const result = fn.apply(this, args);
        cache.set(key, result);

        // Remove oldest if over limit
        if (cache.size > maxSize) {
            const firstKey = cache.keys().next().value;
            cache.delete(firstKey);
        }

        return result;
    };
}

Example 5: Event Handlers and Callbacks

// Closure in event handlers
function setupButton(buttonId, message) {
    const button = document.getElementById(buttonId);

    button.addEventListener('click', function() {
        alert(message);  // Closure over 'message'
    });
}

setupButton('btn1', 'Button 1 clicked!');
setupButton('btn2', 'Button 2 clicked!');

// Debounce using closures
function debounce(fn, delay) {
    let timeoutId;  // Closure over timeout

    return function(...args) {
        clearTimeout(timeoutId);

        timeoutId = setTimeout(() => {
            fn.apply(this, args);
        }, delay);
    };
}

const debouncedSearch = debounce((query) => {
    console.log('Searching for:', query);
}, 300);

// Throttle using closures
function throttle(fn, limit) {
    let inThrottle;

    return function(...args) {
        if (!inThrottle) {
            fn.apply(this, args);
            inThrottle = true;
            setTimeout(() => inThrottle = false, limit);
        }
    };
}

const throttledScroll = throttle(() => {
    console.log('Scroll event');
}, 100);

window.addEventListener('scroll', throttledScroll);

// Once function
function once(fn) {
    let called = false;
    let result;

    return function(...args) {
        if (!called) {
            called = true;
            result = fn.apply(this, args);
        }
        return result;
    };
}

const initialize = once(() => {
    console.log('Initializing...');
    return 'initialized';
});

initialize();  // 'Initializing...'
initialize();  // (nothing, returns 'initialized')

Example 6: Partial Application and Currying

// Partial application
function partial(fn, ...presetArgs) {
    return function(...laterArgs) {
        return fn(...presetArgs, ...laterArgs);
    };
}

function greet(greeting, name, punctuation) {
    return `${greeting}, ${name}${punctuation}`;
}

const sayHello = partial(greet, 'Hello');
const sayHelloToArthur = partial(greet, 'Hello', 'Arthur');

console.log(sayHello('Arthur', '!'));     // 'Hello, Arthur!'
console.log(sayHelloToArthur('!!!'));     // 'Hello, Arthur!!!'

// Currying
function curry(fn) {
    return function curried(...args) {
        if (args.length >= fn.length) {
            return fn.apply(this, args);
        }
        return function(...moreArgs) {
            return curried.apply(this, args.concat(moreArgs));
        };
    };
}

const curriedGreet = curry(greet);

console.log(curriedGreet('Hi')('Sarah')('!'));  // 'Hi, Sarah!'
console.log(curriedGreet('Hey', 'Bob', '.'));   // 'Hey, Bob.'

// Practical currying
const add = curry((a, b, c) => a + b + c);
const add5 = add(5);
const add5and10 = add5(10);

console.log(add5and10(15));  // 30

Key Takeaways:

  • Closures capture variables from outer scope
  • Used for privacy, factories, and state
  • Be aware of closure over reference vs value
  • Foundation for many patterns (memoization, debounce)
  • Modern alternatives: private class fields

Imitation

Challenge 1: Create a Rate Limiter

Task: Implement a rate limiter that allows only N calls per time window.

Solution

function createRateLimiter(maxCalls, timeWindow) {
    const calls = [];  // Closure over call timestamps

    return function(fn) {
        return function(...args) {
            const now = Date.now();

            // Remove calls outside time window
            while (calls.length > 0 && calls[0] <= now - timeWindow) {
                calls.shift();
            }

            if (calls.length >= maxCalls) {
                console.log('Rate limit exceeded');
                return null;
            }

            calls.push(now);
            return fn.apply(this, args);
        };
    };
}

// Usage: max 3 calls per second
const limiter = createRateLimiter(3, 1000);

const limitedFetch = limiter(async (url) => {
    const response = await fetch(url);
    return response.json();
});

// First 3 calls work, 4th is rate limited
limitedFetch('/api/1');
limitedFetch('/api/2');
limitedFetch('/api/3');
limitedFetch('/api/4');  // 'Rate limit exceeded'


Practice

Exercise 1: Implement Compose

Difficulty: Intermediate

Create a compose function that composes multiple functions:

const composed = compose(add1, multiply2, subtract3);
composed(5)  // subtract3(multiply2(add1(5)))

Exercise 2: State Machine with Closures

Difficulty: Advanced

Build a state machine using closures:

  • Define states and transitions
  • Track current state
  • Validate transitions

Summary

What you learned:

  • How closures capture variables
  • Creating private state
  • Function factories
  • Memoization pattern
  • Debounce, throttle, once
  • Partial application and currying

Next Steps:

  • Read: Modules
  • Practice: Implement utility functions
  • Explore: Functional programming

Resources