Higher-Order Functions

As introduced in the chapter on Procedural Programming, a higher-order function either takes functions as arguments, returns a function, or both. Functional programming relies on higher-order functions, and JavaScript uses them everywhere.

Functions as Arguments

The most common use is passing a function as an argument to another function. This passed function is called a callback (we covered callbacks when discussing function expressions in the Procedural Programming chapter).

function processNumbers(numbers, operation) {
  const results = [];
  for (const num of numbers) {
    results.push(operation(num));
  }
  return results;
}

function double(x) {
  return x * 2;
}

function square(x) {
  return x * x;
}

const nums = [1, 2, 3, 4, 5];

console.log(processNumbers(nums, double));  // [2, 4, 6, 8, 10]
console.log(processNumbers(nums, square));  // [1, 4, 9, 16, 25]

In this example, processNumbers is a higher-order function because it accepts a function (operation) as an argument. By passing different functions, we can reuse the same logic with different behaviors.

Using Anonymous Functions as Callbacks

Instead of defining named functions, you can pass anonymous functions directly:

const nums = [1, 2, 3, 4, 5];

const doubled = processNumbers(nums, function (x) {
  return x * 2;
});

const squared = processNumbers(nums, (x) => x * x);

console.log(doubled);  // [2, 4, 6, 8, 10]
console.log(squared);  // [1, 4, 9, 16, 25]

This pattern is common with built-in array methods like map, filter, and forEach.

Functions Returning Functions

A function can also return another function. That is how patterns like function factories and partial application work.

function createMultiplier(factor) {
  return function (number) {
    return number * factor;
  };
}

const double = createMultiplier(2);
const triple = createMultiplier(3);

console.log(double(5)); // 10
console.log(triple(5)); // 15

Here, createMultiplier is a higher-order function that returns a new function. Each returned function keeps access to the factor value it was created with.

Arrow Function Syntax

The same example can be written more concisely with arrow functions:

const createMultiplier = (factor) => (number) => number * factor;

const double = createMultiplier(2);
const triple = createMultiplier(3);

console.log(double(5));  // 10
console.log(triple(5));  // 15

Practical Example: Custom Sorting

Higher-order functions are used throughout JavaScript’s standard library. The sort method is a good example. It accepts a comparison function, and that function decides the order:

const people = [
  { name: "Alice", age: 30 },
  { name: "Bob", age: 25 },
  { name: "Charlie", age: 35 },
];

// Sort by age (ascending)
people.sort((a, b) => a.age - b.age);
console.log(people);
// [{ name: "Bob", age: 25 }, { name: "Alice", age: 30 }, { name: "Charlie", age: 35 }]

// Sort by name (alphabetical)
people.sort((a, b) => a.name.localeCompare(b.name));
console.log(people);
// [{ name: "Alice", age: 30 }, { name: "Bob", age: 25 }, { name: "Charlie", age: 35 }]

The sort method has no default way to compare objects, so accepting a comparison function lets you define the sorting logic.

Why Higher-Order Functions Are Useful

Higher-order functions give us three things:

  • Abstraction: Separate what to do from how to do it
  • Reusability: Write generic functions that work with different behaviors
  • Composition: Build complex operations from simple functions

The next sections cover patterns that build on higher-order functions: inner functions, closures, currying, and composition.