Q1When you write orders.forEach(printOrder), what calls printOrder?
Higher-Order Functions and Callbacks — Passing and Returning Functions
Learn higher-order functions, which take functions as arguments, and callbacks. Covers forEach, a homemade filter, and functions that return functions.
Sometimes you want to do slightly different things with the same list: just display each item, narrow it down by a condition, or add up the amounts. What they share is the loop structure; the only difference is what happens to each item.
This article covers higher-order functions, which take functions as arguments, and callbacks, the functions that get passed in and called.
Passing Logic as an Argument — Callbacks and forEach
Say you want to display a list of orders one by one. for...of works, but then you write the same loop every time you display a list, even though the only thing that changes is how each item is shown.
A higher-order function (a function that takes a function as an argument or returns a function) lets you separate the loop structure from what happens to each item. A function that's passed as an argument and called by the function that received it is called a callback. The array method forEach calls the callback you pass once for each element.
const orders = [
{ id: "A-1101", item: "Notebook", amount: 480 },
{ id: "A-1102", item: "Ballpoint pen", amount: 150 },
{ id: "A-1103", item: "Sticky notes", amount: 320 },
];
// The function handles displaying just one item
const printOrder = (order) => {
console.log(`${order.id} ${order.item} ${order.amount} yen`);
};
// Pass just the function's name. forEach does the calling
orders.forEach(printOrder);
// A-1101 Notebook 480 yen
// A-1102 Ballpoint pen 150 yen
// A-1103 Sticky notes 320 yen
printOrder, and forEach calls it. The only thing that changes between calls is the element in order.The orders.forEach(printOrder) line has no loop condition and no index. To change how items are displayed, you only edit printOrder, and you can pass the same function to other lists unchanged.
forEach Can't Stop Partway
You can't write break inside forEach, and a return only ends the callback for that one item — the remaining elements keep getting processed. For a loop you want to cut off as soon as you find the element you're after, use for...of with break.
Taking a Test as a Function — A Homemade filter and Booleans
Say you want to narrow a list with different conditions: first only products in stock, then only products at 3000 yen or more. If you write a separate loop for each condition, you end up with near-identical copies, right down to the array that collects the results and the push line.
Only the line with the condition changes, so accept it as a test function and collect only the elements for which it returns true. What gets passed to a callback, and in what order, is up to the function that receives and calls it. filterItems below passes just one element to the test function.
const products = [
{ name: "Desk mat", stock: 4 },
{ name: "Monitor stand", stock: 0 },
{ name: "Cable box", stock: 9 },
];
// Takes a test function and collects only the elements it returns true for
function filterItems(items, judge) {
const kept = [];
items.forEach((item) => {
if (judge(item)) {
kept.push(item);
}
});
return kept;
}
const inStock = filterItems(products, (product) => product.stock > 0);
console.log(inStock.length); // 2
console.log(inStock[0].name); // Desk mat
true, the product with 0 gets false, and only the first goes into kept. Whether an element is kept depends on the return value of the function you passed.The word "stock" never appears inside filterItems. If you later want only products with plenty in stock, you just change the test on the calling line to product.stock >= 5, without touching filterItems at all.
Passing a Test You Built — Higher-Order Functions That Return Functions
Sometimes you pass filterItems tests that differ only in the category they compare against: "stationery only", "tableware only", and so on. If you write out the test expression on every call, then renaming the property you compare means editing every one of those calls.
The other form of higher-order function is a function that returns a function. It has the same shape as the function factories from the previous article: you build byCategory, which takes a category and returns a test function. The returned test remembers category as a closure, so you can pass it straight to filterItems.
const items = [
{ name: "Notebook", category: "Stationery" },
{ name: "Mug", category: "Tableware" },
{ name: "Ballpoint pen", category: "Stationery" },
];
// Takes a category and returns a function that tests whether an item is in it
function byCategory(category) {
return (item) => item.category === category;
}
// Nothing is compared yet; what comes back is the test function
console.log(typeof byCategory("Stationery")); // function
// Pass the returned test straight to filterItems from the previous section
console.log(filterItems(items, byCategory("Stationery")).length); // 2
console.log(filterItems(items, byCategory("Tableware")).length); // 1
filterItems(items, byCategory("Stationery"))— builds the test and passes it as the 2nd argument
category— gets "Stationery" on the line that calls itreturn (item) => ...— returns the test function without comparing anything
item— gets one element passed in fromfilterItemsitem.category === category— compares against the remembered category
judge— holds the test function returned bybyCategoryjudge(item)— passes the elements one by one and calls the test
category is set on the line that calls byCategory, and item is set inside filterItems. The place that builds the test and the place that calls it are different.The line that runs byCategory("Stationery") doesn't compare anything yet; the comparison only happens when judge(item) is called inside filterItems. With 3 products, the same test is called 3 times, and category stays "Stationery" while only item changes.
Without Parentheses, Every Item Is Kept
If you write filterItems(items, byCategory), each product gets passed to byCategory as category, and a function comes back as the test result. A function is truthy (any value that isn't falsy), so every product is kept. What you should pass is the test returned by byCategory("Stationery").
One Structure for Everything — A General Function with Swappable Logic
Say you've written two functions that total up shipping fees, one for standard delivery and one for express. The loop and the variable holding the total are the same; the only difference is the expression that works out each order's fee. With this setup, it's easy to fix one and forget to fix the other.
Turn the part that differs into a function parameter, and keep a single copy of the structure. sumShipping takes an array of orders and a function that calculates the fee, passes each order to it, and adds up the return values. The calling side writes only the fee expression, and the receiving side holds only the loop and the total.
const shippingOrders = [{ id: "A-1101", total: 6200 }, { id: "A-1102", total: 3400 }];
// One structure. Only the fee calculation is passed in as a function
function sumShipping(orders, calcFee) {
let total = 0;
for (const order of orders) {
total = total + calcFee(order);
}
return total;
}
const normalFee = (order) => (order.total >= 5000 ? 0 : 500);
const expressFee = () => 800; // Doesn't use order, so the parameter can be left out
const flatFee = () => 300;
console.log(sumShipping(shippingOrders, normalFee)); // 500
console.log(sumShipping(shippingOrders, expressFee)); // 1600
console.log(sumShipping(shippingOrders, flatFee)); // 600
calcFee, the same two orders total 500, 1600, or 600. The code inside sumShipping is identical all three times.Even a change like capping the total needs just one edit in sumShipping, and it applies to all three kinds of fees. The table below shows how each of the three receiving functions in this article calls its callback and uses the return value.
| Receiving Function | How It Calls the Callback | The Callback's Return Value |
|---|---|---|
| forEach | Passes the elements one by one | Not used |
| Homemade filterItems | Passes an element as judge(item) | Collects only the elements that get true |
| Homemade sumShipping | Passes an order as calcFee(order) | Adds it as that order's shipping fee |
Knowledge Check
Answer each question one by one.
Q2In the homemade filterItems, what happens to an element when the test function you passed returns false?
Q3If you pass byCategory without parentheses, as in filterItems(items, byCategory), what does the returned array contain?