Higher-Order Functions
A higher-order function is any function that does at least one of: 1. Takes another function as an argument, or 2. Returns a function as its result.
🎯 What you'll learn
- Definition
- Built-in Higher-Order Functions You Already Use
- Function Composition
- Returning Functions: Configuration and Specialization
- Decorators (Wrapping Functions)
1Definition
A higher-order function is any function that does at least one of:
- Takes another function as an argument, or
- Returns a function as its result.
Functions are first-class values in JavaScript — they can be stored in variables, passed around, and returned, exactly like numbers or strings. This is what makes higher-order functions possible.
function applyTwice(fn, value) { // takes a function as an argument
return fn(fn(value));
}
applyTwice(x => x * 2, 3); // 12
function multiplier(factor) { // returns a function
return x => x * factor;
}
const triple = multiplier(3);
triple(5); // 152Built-in Higher-Order Functions You Already Use
map, filter, reduce, forEach, sort (with a comparator), setTimeout, and addEventListener are all higher-order functions — they accept a callback function as an argument.
[1, 2, 3].map(n => n * 2); // map is higher-order — takes a function
setTimeout(() => console.log("done"), 1000); // setTimeout is higher-order too3Function Composition
Combining small, single-purpose functions into a pipeline is the most practical use of higher-order functions:
const compose = (...fns) => x => fns.reduceRight((acc, fn) => fn(acc), x);
const pipe = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x);
const trim = s => s.trim();
const lower = s => s.toLowerCase();
const exclaim = s => s + "!";
const shout = pipe(trim, lower, exclaim);
shout(" HELLO "); // "hello!"pipe runs left to right; compose runs right to left — both are standard names in functional-programming-flavored JavaScript.
4Returning Functions: Configuration and Specialization
function createValidator(minLength) {
return function (value) {
return typeof value === "string" && value.length >= minLength;
};
}
const isValidPassword = createValidator(8);
isValidPassword("short"); // false
isValidPassword("longenough"); // trueThis pattern — a function that returns a specialized function — is the basis of currying, decorators, and middleware chains (Express middleware and Redux middleware both rely on this exact shape).
5Decorators (Wrapping Functions)
A higher-order function can wrap another function to add behavior without modifying its source:
function withLogging(fn) {
return function (...args) {
console.log(`Calling ${fn.name} with`, args);
const result = fn(...args);
console.log(`${fn.name} returned`, result);
return result;
};
}
const add = (a, b) => a + b;
const loggedAdd = withLogging(add);
loggedAdd(2, 3); // logs the call, then returns 5This is the same principle behind memoize (Closures topic), debounce, and throttle.
6Debounce and Throttle — Practical Higher-Order Functions
function debounce(fn, delayMs) {
let timeoutId;
return function (...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn.apply(this, args), delayMs);
};
}debounce returns a new function that resets its timer on every call, only firing fn once activity has paused — the standard fix for excessive input/resize event handling.
Under the HoodCall-Site Monomorphism for Callbacks▾
The Functions topic's tiered compilation pipeline and the Prototype Chain topic's inline caches both apply directly to callback call sites — and this is where higher-order functions can quietly cost more than a hand-written loop, or, when the engine can optimize well, cost almost nothing extra at all.
Inlining: erasing the function-call overhead entirely
When a call site like arr.map(callback) consistently receives the same callback function every time it runs, the optimizing compiler can inline that callback directly into map's own optimized code — eliminating the function-call overhead altogether, so the generated machine code ends up close to what a manual for loop would produce.
function double(x) { return x * 2; }
const result = [1, 2, 3].map(double); // same `double` function every time this line runs -> inlinableMegamorphic callback sites lose this optimization
function runTransform(arr, transformFn) {
return arr.map(transformFn); // `transformFn` is a DIFFERENT function object on every call
}
runTransform(data1, x => x + 1);
runTransform(data2, x => x * 2);
runTransform(data3, x => x.toUpperCase());Here, the .map() call site inside runTransform sees a different callback identity every invocation — it can never safely assume "it's always this one function," so it can't inline, and falls back to a real indirect function call each time. This is the same monomorphic/polymorphic/megamorphic spectrum from the Prototype Chain topic, applied to functions as the varying thing instead of object shapes.
Why this rarely matters, and when it does
For the overwhelming majority of code, this distinction is invisible — a few extra function-call frames per iteration is negligible next to real work (network calls, DOM updates, business logic). It becomes worth knowing specifically in hot paths: tight loops over large arrays, called extremely often, where a highly polymorphic higher-order pattern (a different closure created and passed in on every call, as debounce/memoize's returned functions are) can prevent the kind of inlining that makes functional-style code as fast as imperative code in the first place.
Closures returned from decorators are fresh objects every time
Every call to debounce(fn, delayMs) (section 6) or memoize(fn) allocates a brand-new function object with its own Context (see the Closures topic) — calling a decorator factory repeatedly (e.g. inside a loop, or on every render in a UI framework) creates a new, independent closure each time, none of which share state with each other, and each of which represents a fresh object the engine has to track from scratch for optimization purposes.
⚠️ Common Pitfalls
- Losing
thisinside a returned function — if the wrapped function relies onthis, usefn.apply(this, args)or convert to arrow functions consistently (see thethiskeyword topic). - Over-composing for its own sake — a 6-function
pipechain that's only used once is often less readable than a plain sequence of statements; reserve composition for genuinely reused logic. - Forgetting closures persist state between calls — a decorator like
debouncekeepstimeoutIdalive across every call via closure; creating a new debounced function on every render (e.g. inline in a React component) defeats the purpose because the closure resets each time. - Confusing "takes a callback" with "is asynchronous" —
map/filter/sortare higher-order but fully synchronous; higher-order-ness and async-ness are unrelated concepts.
📋 Quick Reference
higher-order function = takes a function in, OR returns a function out (or both)
pipe(f, g, h)(x) = h(g(f(x))) — left to right
compose(f, g, h)(x) = f(g(h(x))) — right to left🎤 Interview Q&A
Q1What is a higher-order function?core▾
A function that takes functions as arguments or returns a function: map, filter, reduce, setTimeout, decorators like debounce, memoize, and middleware.
- Why useful? Abstracting behaviour and composing small functions.
Q2Implement debounce.core▾
function debounce(fn, ms) { let t; return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), ms); }; } Calls run only after ms of silence.
- Debounce vs throttle? Debounce waits for quiet; throttle runs at most once per interval.
Q3Implement memoize for single-argument functions.core▾
function memoize(fn) { const cache = new Map(); return x => { if (!cache.has(x)) cache.set(x, fn(x)); return cache.get(x); }; }
- Risks? Unbounded cache growth; use LRU or WeakMap for object keys.
Q4What is function composition?senior▾
Combining functions so the output of one feeds the next: compose(f, g)(x) = f(g(x)), often implemented with reduceRight; pipe applies left to right.
- Where is it common? Functional libraries, middleware pipelines.
Q5Why can chaining map/filter/reduce be inefficient?core▾
Each step creates an intermediate array and iterates again. For huge datasets or hot paths, a single loop or iterator helpers/lazy pipelines avoid extra allocations; for normal sizes readability wins.
- Iterator helpers? New
.map/.filteron iterators evaluate lazily.
🛠️ Practice exercises
Each exercise is a file in this topic folder. Code exercises are self-checking: run them and make the checks pass.