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
Related Resources
Official Documentation
Next Steps
- The ’this’ Keyword and Context Binding
- Arrow Functions and Function Expressions
- Callbacks and Asynchronous JavaScript
Comments