Skip to main content
Adapted, not copied

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, and reduce β€” 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.
  • filter keeps matching elements, map transforms each element 1-to-1, reduce folds everything into a single value.
  • Chaining filter/map/reduce reads as a labeled pipeline; a hand-written loop hides that structure inside imperative steps.
  • forEach is for side effects only β€” it doesn't return a usable array, unlike map.

Try it yourself​

  1. Given the orders array above, use map to produce an array of strings like "Clean Code x2".
  2. Use filter and .length to count how many orders were cancelled (qty === 0).
  3. Use reduce to find the single most expensive order's title (no filter/map needed β€” reduce alone can do it).
Hints
  1. Template literals inside .map(): `${order.title} x${order.qty}`.
  2. orders.filter(o => o.qty === 0).length.
  3. Track the "best order so far" as the accumulator, and compare order.price > accumulator.price on 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.

Β 

🌟 Join the Javascript360 community

Connect with fellow JS developers, and get free interview prep, React challenges, and DSA guides.

Join on WhatsApp β†’