Original notes for JavaScript360.org — topic order inspired by Eloquent JavaScript (4th ed.) by Marijn Haverbeke. See the full attribution note.
Chapter 5: Higher-Order Functions
Estimated time: ~45 minutes · Chapter type: Concept
What you'll learn
- What "higher-order function" actually means and why it matters
map,filter, andreduce— the three you'll use constantly- How to compose small functions into bigger pipelines
- A worked text-processing example
Abstraction: naming a pattern
A higher-order function is simply a function that takes another function as an argument, returns one, or both. You met the idea already in Chapter 3 (functions as values); this chapter is about why that matters in practice.
Consider three loops that all follow the identical shape — walk an array, do something with each element:
function logAll(items) {
for (const item of items) console.log(item);
}
function doubleAll(nums) {
const result = [];
for (const n of nums) result.push(n * 2);
return result;
}
function positivesOnly(nums) {
const result = [];
for (const n of nums) if (n > 0) result.push(n);
return result;
}
The shape — "walk every element, do something" — is identical in all three; only the "something" differs. Higher-order functions let you extract that shared shape once and pass in the "something" as an argument, instead of retyping the loop every time.
Our dataset for this chapter
We'll work with a small list of orders from an imaginary online bookstore:
const orders = [
{ id: 1, title: 'Clean Code', price: 32, qty: 2 },
{ id: 2, title: 'The Pragmatic Programmer', price: 40, qty: 1 },
{ id: 3, title: 'Refactoring', price: 45, qty: 0 }, // cancelled — qty 0
{ id: 4, title: 'Eloquent Notes Vol. 1', price: 20, qty: 3 },
];
filter: keep only what matches
filter builds a new array containing only the elements for which a given function returns true. It doesn't touch the original array.
const activeOrders = orders.filter((order) => order.qty > 0);
// everything except "Refactoring"
map: transform each element
map builds a new array by running a function over every element and collecting the results — always the same length as the input, one output per input.
const titles = orders.map((order) => order.title);
// ["Clean Code", "The Pragmatic Programmer", "Refactoring", "Eloquent Notes Vol. 1"]
const lineItems = orders.map((order) => ({
...order,
lineTotal: order.price * order.qty,
}));
reduce: combine everything into one value
reduce is the most general of the three — it walks the array while carrying an accumulator forward, combining each element into it one at a time, and returns whatever the accumulator ends up as.
const revenue = orders.reduce((total, order) => total + order.price * order.qty, 0);
// total starts at 0 (the second argument), and grows by each order's line total
reduce's callback takes (accumulator, currentElement) and must return the new accumulator for the next step. It's genuinely general-purpose — you can implement map and filter themselves in terms of reduce, though you'd rarely want to in practice:
function mapWithReduce(array, fn) {
return array.reduce((result, item) => [...result, fn(item)], []);
}
Composability: chaining calls together
Because map and filter each return a fresh array, you can chain them directly, reading top to bottom like a pipeline of transformations:
const revenueFromActiveOrders = orders
.filter((order) => order.qty > 0) // step 1: drop cancelled orders
.map((order) => order.price * order.qty) // step 2: compute each line total
.reduce((sum, lineTotal) => sum + lineTotal, 0); // step 3: add them all up
// 32*2 + 40*1 + 20*3 = 164
Compare that to the equivalent hand-written loop:
let sum = 0;
for (const order of orders) {
if (order.qty > 0) {
sum += order.price * order.qty;
}
}
Both do the same work. The chained version names each stage of the transformation (filter → map → reduce), which tends to be easier to read at a glance and easier to modify — inserting a new stage is one extra .method() call rather than restructuring a loop body.
One caveat: chaining multiple map/filter calls does walk the array multiple times, which matters for very large datasets. For everyday sizes (dozens to tens of thousands of items), readability should win; optimize only once you've measured an actual problem.
Other array methods worth knowing
orders.find((o) => o.id === 3); // the first matching element, or undefined
orders.some((o) => o.qty === 0); // true if *any* element matches
orders.every((o) => o.qty > 0); // true only if *all* elements match
orders.forEach((o) => console.log(o.title)); // like a for...of, but as a method — no return value
forEach looks similar to map but is meant purely for side effects (like logging) — it always returns undefined, so don't try to chain off it.
Strings and character codes
Text processing is one of the best places to see higher-order functions at work. Every character has a numeric code point you can inspect with charCodeAt (or codePointAt for characters outside the basic range, like many emoji):
'A'.charCodeAt(0); // 65
String.fromCharCode(65); // "A"
Here's a small Caesar-cipher-style shift function built entirely from higher-order string/array operations — shifting each letter forward by a fixed amount:
function shiftLetters(text, amount) {
return text
.split('')
.map((char) => {
const code = char.charCodeAt(0);
const isUpper = code >= 65 && code <= 90;
const isLower = code >= 97 && code <= 122;
if (!isUpper && !isLower) return char; // leave punctuation/spaces untouched
const base = isUpper ? 65 : 97;
return String.fromCharCode(((code - base + amount) % 26 + 26) % 26 + base);
})
.join('');
}
shiftLetters('Hello, World!', 3); // "Khoor, Zruog!"
.split('') turns the string into an array of characters, .map transforms each one, and .join('') glues the result back into a string — the exact filter/map/reduce mindset applied to text instead of orders.
Key takeaways
- A higher-order function is one that takes or returns another function — it's how you extract a repeated shape of logic and parameterize just the part that changes.
filterkeeps matching elements,maptransforms each element 1-to-1,reducefolds everything into a single value.- Chaining
filter/map/reducereads as a labeled pipeline; a hand-written loop hides that structure inside imperative steps. forEachis for side effects only — it doesn't return a usable array, unlikemap.
Try it yourself
- Given the
ordersarray above, usemapto produce an array of strings like"Clean Code x2". - Use
filterand.lengthto count how many orders were cancelled (qty === 0). - Use
reduceto find the single most expensive order's title (nofilter/mapneeded —reducealone can do it).
Hints
- Template literals inside
.map():`${order.title} x${order.qty}`. orders.filter(o => o.qty === 0).length.- Track the "best order so far" as the accumulator, and compare
order.price > accumulator.priceon each step.
Summary
map, filter, and reduce are the vocabulary you'll use to describe what transformation you want, letting JavaScript handle the how of walking the array. It's a small mental shift from "write a loop" to "describe a pipeline," but it pays off constantly once it clicks. Next, we move from data to behavior: objects that bundle data with methods, and the prototype system underneath JavaScript's take on object-oriented programming.