Skip to main content

Closures: Understanding Function Scope

Created: December 18, 2025 4 min read

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

Resources

Comments

Share this article

Scan to read on mobile