advanced
Step 17 of 20
Closures and Higher-Order Functions
JavaScript Programming
Closures and Higher-Order Functions
Closures and higher-order functions are two of the most powerful concepts in JavaScript that enable elegant patterns like data privacy, currying, memoization, and function composition. A closure is a function that remembers the variables from its outer scope even after the outer function has finished executing. A higher-order function is a function that takes other functions as arguments or returns functions. Together, they form the foundation of functional programming in JavaScript and are used extensively in modern frameworks and libraries.
Understanding Closures
// A closure "closes over" variables from its enclosing scope
function createGreeter(greeting) {
// 'greeting' is captured by the inner function
return function(name) {
return `${greeting}, ${name}!`;
};
}
const hello = createGreeter("Hello");
const hola = createGreeter("Hola");
console.log(hello("Alice")); // "Hello, Alice!"
console.log(hola("Bob")); // "Hola, Bob!"
// 'greeting' is still accessible even though createGreeter has returned
// Counter — data privacy through closures
function createCounter(initialValue = 0) {
let count = initialValue; // Private — cannot be accessed directly
return {
increment() { return ++count; },
decrement() { return --count; },
getCount() { return count; },
reset() { count = initialValue; return count; }
};
}
const counter = createCounter(10);
console.log(counter.increment()); // 11
console.log(counter.increment()); // 12
console.log(counter.decrement()); // 11
console.log(counter.getCount()); // 11
// counter.count is undefined — truly private!
Practical Closure Patterns
// Memoization — cache expensive computation results
function memoize(fn) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
const expensiveCalc = memoize((n) => {
console.log(`Computing for ${n}...`);
return n * n;
});
expensiveCalc(5); // "Computing for 5..." → 25
expensiveCalc(5); // Returns 25 from cache (no log)
// Rate limiter
function rateLimit(fn, delay) {
let lastCall = 0;
return function(...args) {
const now = Date.now();
if (now - lastCall >= delay) {
lastCall = now;
return fn.apply(this, args);
}
};
}
const limitedLog = rateLimit(console.log, 1000);
// limitedLog("hello"); — works
// limitedLog("world"); — ignored if called within 1 second
// Once — function that runs only once
function once(fn) {
let called = false;
let result;
return function(...args) {
if (!called) {
called = true;
result = fn.apply(this, args);
}
return result;
};
}
const initialize = once(() => {
console.log("Initializing...");
return { ready: true };
});
initialize(); // "Initializing..." → { ready: true }
initialize(); // Returns { ready: true } without logging
Higher-Order Functions
// Currying — transform f(a, b, c) into f(a)(b)(c)
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
}
return function(...moreArgs) {
return curried.apply(this, [...args, ...moreArgs]);
};
};
}
const add = curry((a, b, c) => a + b + c);
console.log(add(1)(2)(3)); // 6
console.log(add(1, 2)(3)); // 6
console.log(add(1)(2, 3)); // 6
const add5 = add(5);
console.log(add5(3)(2)); // 10
// Function composition
const pipe = (...fns) => (x) => fns.reduce((v, f) => f(v), x);
const processName = pipe(
str => str.trim(),
str => str.toLowerCase(),
str => str.replace(/\s+/g, "-"),
str => str.slice(0, 20)
);
console.log(processName(" Hello World ")); // "hello-world"
// Partial application
function partial(fn, ...presetArgs) {
return function(...laterArgs) {
return fn(...presetArgs, ...laterArgs);
};
}
const multiply = (a, b) => a * b;
const double = partial(multiply, 2);
const triple = partial(multiply, 3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
Pro tip: Closures are the key to data privacy in JavaScript. Before private class fields (#) existed, closures were the only way to create truly private variables. They are still valuable in functional programming patterns like memoization, currying, and creating configurable functions that remember their settings.
Key Takeaways
- A closure is a function that retains access to its outer scope's variables even after the outer function returns.
- Closures enable data privacy by encapsulating state that can only be accessed through returned functions.
- Memoization, rate limiting, and once-only execution are practical closure patterns used in production code.
- Higher-order functions accept or return functions, enabling patterns like currying and composition.
- Function composition with
pipe()orcompose()chains small functions into powerful data transformations.