Skip to main content

JavaScript Closures: Complete Guide

🔐 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 finished executing. It "closes over" the variables from its lexical environment.

Simple Definition:

A closure gives you access to an outer function's scope from an inner function.


🏗️ Basic Closure Example

function outerFunction(x) {
// Outer function's variable
const outerVariable = x;

// Inner function (closure)
function innerFunction(y) {
// Can access outerVariable even after outerFunction returns
return outerVariable + y;
}

return innerFunction;
}

const addFive = outerFunction(5);
console.log(addFive(10)); // 15

// outerFunction has finished executing, but innerFunction
// still has access to outerVariable (5)

What happens:

  1. outerFunction(5) executes and returns innerFunction
  2. outerFunction finishes executing and is removed from call stack
  3. addFive now holds reference to innerFunction
  4. When addFive(10) is called, it still remembers outerVariable = 5

🔍 How Closures Work (Under the Hood)

Lexical Scoping:

function outer() {
const message = "Hello";

function inner() {
console.log(message); // Can access 'message' from outer scope
}

return inner;
}

const myClosure = outer();
myClosure(); // "Hello" - still has access to 'message'

Memory Management:

function createCounter() {
let count = 0; // This variable is "trapped" in closure

return function() {
count++; // Closure keeps 'count' alive
return count;
};
}

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

console.log(counter1()); // 1
console.log(counter1()); // 2
console.log(counter2()); // 1 (separate closure, separate count)
console.log(counter1()); // 3

Key Points:

  • Each call to createCounter() creates a new closure
  • Each closure has its own copy of the count variable
  • The count variable stays in memory as long as the closure exists

📚 Practical Closure Examples

1. Data Privacy (Encapsulation)

function createBankAccount(initialBalance) {
let balance = initialBalance; // Private variable

return {
deposit(amount) {
if (amount > 0) {
balance += amount;
return balance;
}
throw new Error("Amount must be positive");
},

withdraw(amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
return balance;
}
throw new Error("Invalid withdrawal amount");
},

getBalance() {
return balance; // Only way to access balance
}
};
}

const account = createBankAccount(100);
console.log(account.getBalance()); // 100
account.deposit(50);
console.log(account.getBalance()); // 150

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

2. Function Factories

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

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

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

3. Module Pattern

const TodoModule = (function() {
let todos = []; // Private array
let nextId = 1; // Private counter

return {
add(text) {
const todo = {
id: nextId++,
text: text,
completed: false
};
todos.push(todo);
return todo;
},

remove(id) {
todos = todos.filter(todo => todo.id !== id);
},

toggle(id) {
const todo = todos.find(todo => todo.id === id);
if (todo) {
todo.completed = !todo.completed;
}
},

getAll() {
return [...todos]; // Return copy to prevent mutation
},

getCount() {
return todos.length;
}
};
})();

TodoModule.add("Learn JavaScript");
TodoModule.add("Master Closures");
console.log(TodoModule.getAll());
console.log(TodoModule.getCount()); // 2

4. Event Handlers with State

function createButtonHandler(name) {
let clickCount = 0;

return function() {
clickCount++;
console.log(`${name} clicked ${clickCount} times`);
};
}

// HTML: <button id="btn1">Button 1</button>
// HTML: <button id="btn2">Button 2</button>

document.getElementById('btn1').onclick = createButtonHandler('Button 1');
document.getElementById('btn2').onclick = createButtonHandler('Button 2');

// Each button maintains its own click count

⚠️ Common Closure Pitfalls

1. Loop Problem (Classic Issue)

// ❌ PROBLEM: All functions log 3
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // Always logs 3 (the final value of i)
}, 1000);
}

// ✅ SOLUTION 1: Use let (block scope)
for (let i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // Logs 0, 1, 2
}, 1000);
}

// ✅ SOLUTION 2: Use closure with IIFE
for (var i = 0; i < 3; i++) {
(function(index) {
setTimeout(function() {
console.log(index); // Logs 0, 1, 2
}, 1000);
})(i);
}

// ✅ SOLUTION 3: Use closure function
function createLogger(index) {
return function() {
console.log(index);
};
}

for (var i = 0; i < 3; i++) {
setTimeout(createLogger(i), 1000);
}

2. Memory Leaks

// ❌ PROBLEM: Potential memory leak
function attachListeners() {
const largeData = new Array(1000000).fill('data');

document.getElementById('button').onclick = function() {
// This closure keeps largeData in memory even if we don't use it
console.log('Button clicked');
};
}

// ✅ SOLUTION: Don't reference unnecessary variables
function attachListeners() {
const largeData = new Array(1000000).fill('data');

// Process data if needed
const processedData = largeData.length; // Use only what you need

document.getElementById('button').onclick = function() {
console.log(`Button clicked, data size was: ${processedData}`);
// largeData is not referenced, can be garbage collected
};
}

🎯 Advanced Closure Patterns

1. Currying

function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
} else {
return function(...nextArgs) {
return curried(...args, ...nextArgs);
};
}
};
}

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

const curriedAdd = curry(add);

console.log(curriedAdd(1)(2)(3)); // 6
console.log(curriedAdd(1, 2)(3)); // 6
console.log(curriedAdd(1)(2, 3)); // 6

2. Memoization

function memoize(fn) {
const 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;
};
}

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

const memoizedFib = memoize(fibonacci);

console.log(memoizedFib(10)); // Computing... 55
console.log(memoizedFib(10)); // Cache hit! 55

3. Throttling/Debouncing

function debounce(func, delay) {
let timeoutId;

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

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

function throttle(func, delay) {
let lastCall = 0;

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

if (now - lastCall >= delay) {
lastCall = now;
func.apply(this, args);
}
};
}

// Usage
const debouncedSearch = debounce(function(query) {
console.log(`Searching for: ${query}`);
}, 300);

const throttledScroll = throttle(function() {
console.log('Scrolling...');
}, 100);

🔧 Closure vs Other Concepts

Closure vs Regular Function:

// Regular function - no closure
function regularFunction() {
const data = "I'm temporary";
return data;
}

console.log(regularFunction()); // "I'm temporary"
// 'data' is destroyed after function execution

// Closure function
function closureFunction() {
const data = "I persist!";

return function() {
return data; // Closure keeps 'data' alive
};
}

const closure = closureFunction();
console.log(closure()); // "I persist!" - data is still accessible

Closure vs Class:

// Using Closure
function createPersonClosure(name, age) {
return {
getName() { return name; },
getAge() { return age; },
setAge(newAge) { age = newAge; },
greet() { return `Hi, I'm ${name}, ${age} years old`; }
};
}

// Using Class
class PersonClass {
constructor(name, age) {
this.name = name;
this.age = age;
}

getName() { return this.name; }
getAge() { return this.age; }
setAge(newAge) { this.age = newAge; }
greet() { return `Hi, I'm ${this.name}, ${this.age} years old`; }
}

const personClosure = createPersonClosure("Alice", 25);
const personClass = new PersonClass("Bob", 30);

console.log(personClosure.greet()); // "Hi, I'm Alice, 25 years old"
console.log(personClass.greet()); // "Hi, I'm Bob, 30 years old"

📊 Closure Use Cases Summary

Use CaseBenefitExample
Data PrivacyEncapsulation, hide internal stateBank account, user session
Function FactoriesCreate specialized functionsMultipliers, validators
Module PatternOrganize code, namespaceLibraries, APIs
Event HandlersMaintain state between eventsClick counters, form handlers
CallbacksPreserve contextAsync operations, timers
MemoizationCache expensive computationsFibonacci, API responses
CurryingPartial function applicationUtility functions
Debouncing/ThrottlingControl function execution rateSearch, scroll handlers

🎯 Best Practices

1. Use Closures for Data Privacy

// ✅ Good: Private variables
function createConfig() {
let settings = { theme: 'dark', lang: 'en' };

return {
get(key) { return settings[key]; },
set(key, value) { settings[key] = value; }
};
}

2. Be Mindful of Memory Usage

// ✅ Good: Only capture what you need
function createHandler(id) {
return function() {
console.log(`Handler for ${id}`);
};
}

// ❌ Avoid: Capturing unnecessary large objects
function createHandler(user) {
const largeUserData = user.getAllData(); // Large object

return function() {
console.log(`Handler for ${user.id}`); // Only need id
};
}

3. Use Modern Syntax When Appropriate

// Modern arrow function closure
const createMultiplier = (factor) => (number) => number * factor;

const double = createMultiplier(2);
console.log(double(5)); // 10

Closures are a fundamental JavaScript concept that enables powerful patterns like data privacy, function factories, and module systems. Understanding closures is essential for writing effective JavaScript code and understanding how many JavaScript features work under the hood.