Learning Objectives

  • Create practical closures from scratch
  • Understand common closure patterns
  • Build functions that return functions
  • Use closures to maintain state

Building Your First Closure

Now that you understand scope and lexical environment, let's create some practical closures. We'll start simple and build up to more complex examples.

Pattern 1: Simple Counter

The classic closure example - a counter with private state:

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

// Create another independent counter
const counter2 = createCounter();
console.log(counter2()); // 1
console.log(counter2()); // 2

Each counter maintains its own private count variable. They don't interfere with each other!

Pattern 2: Function with Configuration

Create specialized functions based on configuration:

function createGreeter(greeting) {
    return function(name) {
        return `${greeting}, ${name}!`;
    };
}

const sayHello = createGreeter("Hello");
const sayHi = createGreeter("Hi");
const sayHowdy = createGreeter("Howdy");

console.log(sayHello("Alice"));  // "Hello, Alice!"
console.log(sayHi("Bob"));       // "Hi, Bob!"
console.log(sayHowdy("Charlie")); // "Howdy, Charlie!"

Pattern 3: Multiple Methods

Return an object with multiple methods that share access to private data:

function createBankAccount(initialBalance) {
    let balance = initialBalance; // Private
    
    return {
        deposit: function(amount) {
            if (amount > 0) {
                balance += amount;
                return `Deposited $${amount}. New balance: $${balance}`;
            }
            return "Invalid amount";
        },
        
        withdraw: function(amount) {
            if (amount > 0 && amount <= balance) {
                balance -= amount;
                return `Withdrew $${amount}. New balance: $${balance}`;
            }
            return "Invalid amount or insufficient funds";
        },
        
        getBalance: function() {
            return balance;
        }
    };
}

const myAccount = createBankAccount(100);
console.log(myAccount.deposit(50));   // "Deposited $50. New balance: $150"
console.log(myAccount.withdraw(30));  // "Withdrew $30. New balance: $120"
console.log(myAccount.getBalance());  // 120

// Can't access balance directly
console.log(myAccount.balance); // undefined

Practical Example: ID Generator

Create a unique ID generator using closures:

function createIDGenerator(prefix = "ID") {
    let currentID = 0;
    
    return function() {
        currentID++;
        return `${prefix}-${currentID}`;
    };
}

const userID = createIDGenerator("USER");
const orderID = createIDGenerator("ORDER");

console.log(userID());  // "USER-1"
console.log(userID());  // "USER-2"
console.log(orderID()); // "ORDER-1"
console.log(userID());  // "USER-3"
console.log(orderID()); // "ORDER-2"

Practical Example: Rate Limiter

Use closures to implement a simple rate limiter:

function createRateLimiter(maxCalls, timeWindow) {
    let calls = [];
    
    return function(fn) {
        const now = Date.now();
        
        // Remove calls outside the time window
        calls = calls.filter(time => now - time < timeWindow);
        
        if (calls.length < maxCalls) {
            calls.push(now);
            return fn();
        } else {
            return "Rate limit exceeded. Try again later.";
        }
    };
}

// Allow 3 calls per second
const limiter = createRateLimiter(3, 1000);

function apiCall() {
    return "API call successful!";
}

console.log(limiter(apiCall)); // "API call successful!"
console.log(limiter(apiCall)); // "API call successful!"
console.log(limiter(apiCall)); // "API call successful!"
console.log(limiter(apiCall)); // "Rate limit exceeded..."

Practical Example: Memoization

Cache function results using closures:

function memoize(fn) {
    const cache = {};
    
    return function(...args) {
        const key = JSON.stringify(args);
        
        if (key in cache) {
            console.log("Returning cached result");
            return cache[key];
        }
        
        console.log("Computing result");
        const result = fn(...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(fibonacci);

console.log(memoizedFib(10)); // Computing result: 55
console.log(memoizedFib(10)); // Returning cached result: 55

Common Mistakes to Avoid

Mistake 1: Forgetting to Return the Function

// ❌ Wrong
function createCounter() {
    let count = 0;
    function increment() {
        count++;
        return count;
    }
    // Forgot to return increment!
}

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

Mistake 2: Not Understanding Independent Closures

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

const counter1 = createCounter();
const counter2 = createCounter();

console.log(counter1()); // 1
console.log(counter1()); // 2
console.log(counter2()); // 1 (not 3!)
// Each closure has its own count variable

Key Takeaways

  • ✅ Closures enable private variables and data encapsulation
  • ✅ Functions can return other functions that maintain access to outer scope
  • ✅ Each closure instance has its own independent copy of variables
  • ✅ Closures are perfect for configuration and state management
  • ✅ Common patterns: counters, generators, memoization, rate limiting

Next Steps

You've now created several practical closures! In the next lesson, we'll dive deeper into how closures work under the hood, exploring the JavaScript engine's execution context and memory management.