JavaScript Closures Tutorial - Section 1: Fundamentals
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.
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!
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!"
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
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"
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..."
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
// ❌ 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;
};
}
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
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.