Learn by reading through in order

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
The Same printOrder Called Three Times
1st item:A-1101order holdsA-1101Displays A-1101Notebook 480 yen2nd item:A-1102order holdsA-1102Displays A-1102Ballpoint pen150 yen3rd item:A-1103order holdsA-1103Displays A-1103Sticky notes320 yen
You pass just one function, 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.

Display a list of support tickets one by one. tickets is already declared.

① Write a function printTicket that displays one ticket in the form "[T-01] Where is my delivery? (resolved)". If it isn't resolved yet, show (open) instead.

② Pass printTicket to forEach to display every ticket.

③ Count the resolved tickets, and display the count in the form "Resolved: N".

(If it runs correctly, an explanation will appear.)

JavaScript / TypeScript Editor

Run code to see output

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
The Return Value Decides Where Each Element Goes
Desk matwith 4 in stockTestsstock > 04 > 0 istrueGoes intokeptMonitor standwith 0 in stockTestsstock > 00 > 0 isfalseDoesn't gointo kept
The product with 4 in stock gets 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.

Count the shipments in a delivery schedule that meet a condition. shipments is already declared.

① Write countIf, which takes a test function and returns how many elements meet the condition.

② Display how many shipments go out the same day.

③ Display how many shipments have a fee of 4000 yen or more.

④ Put a function that tests for shipments that don't go out the same day into a variable isNextDay, then pass it to countIf and display the count.

JavaScript / TypeScript Editor

Run code to see output

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
Where Each Value in the Test Gets Set
Outermost level (not inside any braces)
  • filterItems(items, byCategory("Stationery")) — builds the test and passes it as the 2nd argument
Inside byCategory
  • category — gets "Stationery" on the line that calls it
  • return (item) => ... — returns the test function without comparing anything
Inside the returned test function
  • item — gets one element passed in from filterItems
  • item.category === category — compares against the remembered category
Inside filterItems
  • judge — holds the test function returned by byCategory
  • judge(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").

Narrow down the orders eligible for members-only free shipping. orders and filterItems are already declared.

① Write both, which takes two tests and returns a test that returns true only when both are met.

② Store two tests in variables: one for orders of 5000 yen or more, and one for members.

③ Combine the tests from ② with both, pass the result to filterItems, and display the ID of each remaining order.

④ Run the test from ③ on the second order, and display the result.

JavaScript / TypeScript Editor

Run code to see output

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
Three Fee Functions, One Structure
sumShipping(orders,calcFee): just onecalcFee getsnormalFeecalcFee getsexpressFeecalcFee getsflatFee0 at 5000 yen ormore, else 500800 regardlessof the order300 regardlessof the orderTotal is 500Total is 1600Total is 600
Depending on the function passed as 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 FunctionHow It Calls the CallbackThe Callback's Return Value
forEachPasses the elements one by oneNot used
Homemade filterItemsPasses an element as judge(item)Collects only the elements that get true
Homemade sumShippingPasses an order as calcFee(order)Adds it as that order's shipping fee

From a part-timer's shift records, display the pay for each shift and work out the total. shifts is already declared.

① Write payReport, which takes the shift records and a function that calculates pay, displays each one in the form "N hours: N yen", and then returns the total.

② Pass a calculation at 1100 yen per hour, and display the returned total in the form "Total N yen".

③ Pass a calculation where only night shifts pay 1.25 times as much, and display the total in the same form.

JavaScript / TypeScript Editor

Run code to see output
QUIZ

Knowledge Check

Answer each question one by one.

Q1When you write orders.forEach(printOrder), what calls printOrder?

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?