Skip to main content
โšก Calmops

Closures: Understanding Function Scope

Closures: Understanding Function Scope

A closure is a function that has access to variables from its outer scope, even after the outer function has returned. Closures are one of JavaScript’s most powerful features.

What is a Closure?

A closure is created every time a function is created:

function outer() {
    const message = "Hello";
    
    function inner() {
        console.log(message); // Can access outer's variable
    }
    
    return inner;
}

const fn = outer();
fn(); // "Hello"

The inner function “closes over” the message variable.

How Closures Work

When a function is created, it maintains a reference to its outer scope:

function makeCounter() {
    let count = 0; // This variable is "closed over"
    
    return function() {
        count++;
        return count;
    };
}

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

Each call to makeCounter() creates a new closure with its own count variable.

Practical Closure Examples

Data Privacy

function createBankAccount(initialBalance) {
    let balance = initialBalance; // Private variable
    
    return {
        deposit(amount) {
            balance += amount;
            return balance;
        },
        withdraw(amount) {
            balance -= amount;
            return balance;
        },
        getBalance() {
            return balance;
        }
    };
}

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

Function Factory

function createMultiplier(multiplier) {
    return function(number) {
        return number * multiplier;
    };
}

const double = createMultiplier(2);
const triple = createMultiplier(3);

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

Event Handler with State

function setupButton(buttonId) {
    let clickCount = 0;
    
    const button = document.getElementById(buttonId);
    button.addEventListener("click", function() {
        clickCount++;
        console.log(`Button clicked ${clickCount} times`);
    });
}

setupButton("myButton");

Memoization

function createMemoizedAdd() {
    const cache = {};
    
    return function(a, b) {
        const key = `${a},${b}`;
        
        if (key in cache) {
            console.log("From cache");
            return cache[key];
        }
        
        console.log("Computing");
        const result = a + b;
        cache[key] = result;
        return result;
    };
}

const add = createMemoizedAdd();
console.log(add(2, 3)); // Computing, 5
console.log(add(2, 3)); // From cache, 5

Common Closure Patterns

Module Pattern

const calculator = (function() {
    let result = 0;
    
    return {
        add(x) {
            result += x;
            return this;
        },
        subtract(x) {
            result -= x;
            return this;
        },
        multiply(x) {
            result *= x;
            return this;
        },
        getResult() {
            return result;
        }
    };
})();

console.log(calculator.add(5).multiply(2).subtract(3).getResult()); // 7

Revealing Module Pattern

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

counter.increment();
counter.increment();
console.log(counter.getCount()); // 2

Partial Application

function partial(fn, ...args) {
    return function(...moreArgs) {
        return fn(...args, ...moreArgs);
    };
}

function add(a, b, c) {
    return a + b + c;
}

const add5 = partial(add, 5);
console.log(add5(3, 2)); // 10

const add5and3 = partial(add, 5, 3);
console.log(add5and3(2)); // 10

Closure Gotchas

Loop Variable Closure

// Problem - all closures reference the same i
const functions = [];
for (var i = 0; i < 3; i++) {
    functions.push(function() {
        return i;
    });
}

console.log(functions[0]()); // 3
console.log(functions[1]()); // 3
console.log(functions[2]()); // 3

Solution 1: Use let

const functions = [];
for (let i = 0; i < 3; i++) {
    functions.push(function() {
        return i;
    });
}

console.log(functions[0]()); // 0
console.log(functions[1]()); // 1
console.log(functions[2]()); // 2

Solution 2: IIFE

const functions = [];
for (var i = 0; i < 3; i++) {
    functions.push((function(j) {
        return function() {
            return j;
        };
    })(i));
}

console.log(functions[0]()); // 0
console.log(functions[1]()); // 1
console.log(functions[2]()); // 2

Memory Considerations

Closures keep references to outer variables, which can affect memory:

function createLargeArray() {
    const largeArray = new Array(1000000).fill(0);
    
    return function() {
        return largeArray.length;
    };
}

const fn = createLargeArray();
// largeArray is kept in memory as long as fn exists

Practical Real-World Examples

Debounce Function

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

const handleSearch = debounce(function(query) {
    console.log("Searching for:", query);
}, 300);

handleSearch("javascript");
handleSearch("javascript closures");
// Only the last call executes after 300ms

Throttle Function

function throttle(fn, delay) {
    let lastCall = 0;
    
    return function(...args) {
        const now = Date.now();
        if (now - lastCall >= delay) {
            fn(...args);
            lastCall = now;
        }
    };
}

const handleScroll = throttle(function() {
    console.log("Scroll event");
}, 1000);

window.addEventListener("scroll", handleScroll);

Once Function

function once(fn) {
    let called = false;
    let result;
    
    return function(...args) {
        if (!called) {
            called = true;
            result = fn(...args);
        }
        return result;
    };
}

const initialize = once(function() {
    console.log("Initializing...");
    return "initialized";
});

console.log(initialize()); // "Initializing...", "initialized"
console.log(initialize()); // "initialized"
console.log(initialize()); // "initialized"

Summary

  • Closure: function with access to outer scope variables
  • Created: every time a function is created
  • Use cases: data privacy, function factories, memoization
  • Patterns: module pattern, revealing module, partial application
  • Gotchas: loop variables, memory considerations
  • Real-world: debounce, throttle, once

Official Documentation

Next Steps

  1. The ’this’ Keyword and Context Binding
  2. Arrow Functions and Function Expressions
  3. Callbacks and Asynchronous JavaScript

Comments