Understanding Higher-Order Functions: Building map, filter, forEach, and reduce from Scratch

Published on:

Have you ever wondered what makes JavaScript’s array methods like map, filter, and reduce so powerful? The answer lies in a fundamental concept called higher-order functions.

What Are Higher-Order Functions?

A higher-order function is simply a function that either:

  1. Takes another function as an argument, or
  2. Returns a function as its result

Think of it like this: instead of just passing data around, we can pass behavior itself. This makes our code more flexible, reusable, and expressive.

In JavaScript, functions are first-class citizens—meaning they can be treated just like any other value (numbers, strings, objects). You can store them in variables, pass them as arguments, and return them from other functions. This is what enables higher-order functions to exist.

Why Higher-Order Functions Matter

Pondering over daily life, nature, or even our own bodies, we observe that everything follows a pattern: input → process → output. In programming, functions represent these processes. But what makes JavaScript special is that functions can operate on other functions, creating a powerful way to abstract and compose behavior.

Let’s explore the two main ways functions can be higher-order:

1. Functions That Return Functions

Sometimes, you want to create a specialized function from a more general one. Here’s a classic example:

function multiplyByBase(x) {
  return function (y) {
    return x * y;
  };
}

// Create specialized functions
const multiplyBy5 = multiplyByBase(5);
const multiplyBy10 = multiplyByBase(10);

console.log(multiplyBy5(10)); // Output: 50
console.log(multiplyBy5(5)); // Output: 25
console.log(multiplyBy10(3)); // Output: 30

This pattern is incredibly powerful and introduces us to the concept of closures (where the inner function “remembers” the outer function’s variables). We’ll explore closures in more detail in future articles.

2. Functions That Accept Functions as Arguments

This is where things get really interesting. By passing functions as arguments, we can create flexible, reusable code that works with different behaviors:

const numbers = [1, 2, 3];

function classifyNumbers(items, testFunction) {
  const result = [];

  for (const num of items) {
    if (testFunction(num)) {
      result.push("Even");
    } else {
      result.push("Odd");
    }
  }

  return result;
}

function checkEven(number) {
  return number % 2 === 0;
}

const result = classifyNumbers(numbers, checkEven);
console.log(result); // ['Odd', 'Even', 'Odd']

Notice how we separated the logic of “what to do” (classifyNumbers) from “how to test” (checkEven). This separation makes our code more modular and testable. In larger projects, this pattern becomes essential for handling complex business logic.

Array Methods: Higher-Order Functions in Action

JavaScript’s array methods are perfect examples of higher-order functions. They accept functions as arguments and apply them to array elements. Let’s build these methods from scratch to truly understand how they work.

Implementing forEach

forEach is the simplest array method. It just executes a function for each element in the array:

const forEach = (arr, callback) => {
  for (let i = 0; i < arr.length; i++) {
    callback(arr[i], i, arr); // Pass element, index, and array
  }
};

const items = [1, 2, 3, 4];

// Using our custom forEach
forEach(items, (item) => {
  console.log(item); // Prints each number on a new line
});

// Or pass an existing function directly
forEach(items, console.log);

The beauty of forEach is its simplicity—it’s just a loop that applies a function to each element. Notice how we can pass console.log directly as the callback function!

Implementing filter

filter creates a new array containing only the elements that pass a test (called a predicate function):

const filter = (arr, predicate) => {
  const result = [];
  for (let i = 0; i < arr.length; i++) {
    if (predicate(arr[i], i, arr)) {
      result.push(arr[i]);
    }
  }
  return result;
};

const isEven = (number) => {
  return number % 2 === 0;
};

const numbers = [1, 2, 3, 4, 5, 6];

const evenNumbers = filter(numbers, isEven);
console.log(evenNumbers); // [2, 4, 6]

The predicate function (isEven in this case) returns true or false for each element. Elements that return true are included in the new array.

Implementing map

map transforms each element of an array by applying a function to it, returning a new array with the transformed values:

const map = (arr, mapper) => {
  const result = [];
  for (let i = 0; i < arr.length; i++) {
    result.push(mapper(arr[i], i, arr));
  }
  return result;
};

const square = (number) => {
  return number * number;
};

const numbers = [1, 2, 3, 4];

const squared = map(numbers, square);
console.log(squared); // [1, 4, 9, 16]

map is one of the most useful array methods. It takes an array and a transformation function, then returns a new array where each element has been transformed. The original array remains unchanged.

Implementing reduce

reduce is the most powerful array method. It “reduces” an array to a single value by applying a function that accumulates a result:

const reduce = (arr, reducer, initialValue) => {
  let accumulator = initialValue;
  for (let i = 0; i < arr.length; i++) {
    accumulator = reducer(accumulator, arr[i], i, arr);
  }
  return accumulator;
};

const add = (sum, number) => {
  return sum + number;
};

const numbers = [1, 2, 3, 4];

const sum = reduce(numbers, add, 0);
console.log(sum); // 10

The initialValue is crucial—it determines:

  • The type of result: Use 0 for numbers, [] for arrays, {} for objects
  • The starting point: The accumulator begins with this value

reduce is incredibly versatile. You can use it to sum numbers, flatten arrays, group objects, or even build other array methods!

Putting It All Together

Here’s a practical example using our custom functions together:

const products = [
  { name: "Laptop", price: 999 },
  { name: "Phone", price: 599 },
  { name: "Tablet", price: 299 },
  { name: "Watch", price: 199 },
];

// Get products under $500
const affordableProducts = filter(products, (product) => product.price < 500);

// Get just the prices
const prices = map(affordableProducts, (product) => product.price);

// Calculate total
const total = reduce(prices, add, 0);

console.log(`Total for affordable products: $${total}`); // Total: $498

Conclusion

Higher-order functions are at the heart of modern JavaScript. They enable us to write code that’s:

  • More readable: Express what we want, not how to do it
  • More reusable: Functions can work with different behaviors
  • More testable: Small, focused functions are easier to test

By building these array methods from scratch, we’ve seen that they’re not magic—they’re just clever applications of higher-order functions. Understanding this concept will help you write better JavaScript code and appreciate the elegance of functional programming patterns.

The next time you use map, filter, or reduce, remember: you’re using higher-order functions to make your code more expressive and powerful!

References:

[1] Eloquent JavaScript - Higher-Order Functions

[2] MDN - Array Methods