According to the definition, a Higher Order Function (HOF) in JavaScript, is a function that accepts one or more functions as an argument or returns a function.
HOFs are very useful because they allow you to write cleaner and more expressive code by utilising code reuse and functional programming principles.
In this article we will demonstrate the four most common HOFs in JavaScript, these are:
- map
- filter
- reduce
- flatMap
These functions are your Swiss knife for writing business logic to transform data, for example from a database into a more suitable form for a user interface and vice versa.
They can simplify your code in a great extend, but you need to be careful to use the right function for the right use case. For this reason, we will introduce each example with a graphical mental model to help you decide when to use each.
1. map
Use map when you have an array of data and you need to transform each element one by one into another array with the same size, like it is demonstrated in the picture bellow.
For instance let’s say we have an array of customers. Each customer object has 3 attributes, first name, last name and age.
const customers = [
{ firstName: "Alice", lastName: "Queen", age: 35 },
{ firstName: "Bob", lastName: "Smith", age: 42 },
{ firstName: "Charlie", lastName: "Brown", age: 28 },
{ firstName: "Diana", lastName: "Prince", age: 30 },
{ firstName: "Ethan", lastName: "Hunt", age: 45 },
{ firstName: "Fiona", lastName: "Gallagher", age: 27 },
{ firstName: "George", lastName: "Clark", age: 39 },
{ firstName: "Hannah", lastName: "Montana", age: 22 },
{ firstName: "Ian", lastName: "McKellen", age: 60 },
{ firstName: "Jack", lastName: "Sparrow", age: 50 }
];
We want a new array with only the age of each person, in this case we will use map to transform the customer objects into numbers and save the result in a new array called ages.
const ages = customers.map(customer => customer.age);
console.log(ages);
> (10) [35, 42, 28, 30, 45, 27, 39, 22, 60, 50]
The transformative part is the arrow function, that we passed as the first parameter. The map function iterates over the array and applies the arrow function in each element.
We could similarly create a new function to get the customer’s age to make the code more expressive.
function getAge(customer) {
return customer.age;
}
const ages = customers.map(getAge);
In this example it doesn’t make a lot of sense to extract the arrow function into a new function, but in a more complicated transformation this would be very beneficial.
Also note that the original customers array did not change, instead a new array with the customer ages has been created, this is a principle of functional programming and helps you write better code by eliminating mutations.
From now on, keep in mind that every function we pass as an argument into those HOFs does not mutate the original data. It is a very bad practice to use functions that mutate data in any of those HOFs.
2. filter
Use filter when you have an array of data and you need a new array that has smaller size and only contains the elements of the first array that fulfil a condition, the rest is filtered out.
Following the example above, let’s say that we need the customers who are older than 30. In this case we will use filter and pass a function that returns a boolean.
const oldCustomers = customers.filter(customer => customer.age > 30);
console.log(customers.length);
console.log(oldCustomers.length);
> 10
6
The filter function will apply the arrow function in every element of the array. If the result is true, then the current element will exist in the new array.
In the same way like before, we can make our code more expressive by extracting the arrow function.
function isOlderThan30(customer) {
return customer.age > 30;
}
const oldCustomers = customers.filter(isOlderThan30);
3. reduce
Use reduce whenever you have an array and need to extract a single value out.
The following examples are some typical use cases for using reduce:
- Get the sum of an arithmetical value.
- Compute the average value.
- Find the minimum value.
Following our customers example, let’s try to use reduce to compute the average age.
const ages = customers.map(customer => customer.age);
const averageAge = ages.reduce((previousValue, currentValue) => previousValue + currentValue, 0) / ages.length;
console.log(averageAge);
> 37.8
The most important to notice here are the following two points:
- An arrow function computes the sum of two numbers.
- The initial value is 0.
The reduce function will start from the first element and the initial value and then move one element at a time. Every time the arrow function is applied and the result is saved and passed in the next iteration as the previous value.
The above example will look much better if we extract the arrow function into a utility sum function.
function sum(a, b) {
return a + b;
}
const averageAge = ages.reduce(sum, 0) / ages.length;
As a second example, let’s say we need to find the minimum age. We will use the reduce function as well.
const ages = customers.map(customer => customer.age);
const minimumAge = ages.reduce((a1, a2) => a2 < a1 ? a2 : a1, ages[0]);
console.log(minimumAge);
> 22
This time we need an arrow function to compare two ages and return the smallest one. Note that the initial value is the first element of the array and not 0, because if it was 0, it would return a wrong result, because every age is obviously greater than 0.
The reduce function is the most challenging to remember. In order to remember it, always keep in mind the sum example.
const sum = numbers.reduce(
(accumulator, currentValue) => accumulator + currentValue,
initialValue,
);
4. flatMap
Last but not least, use the flatMap function whenever you have an array of items and each of those items can be for example another array. If your goal is to create a new array having as items the items in the arrays, then flatMap will do the job. As a rule of thumb, whenever you want a new array with more elements than the original, use flapMap.
The dataset of our example is not very good to demonstrate the use of flatMap. So, we will extend the original dataset with a new attribute called pets.
{ firstName: "Alice", lastName: "Queen", age: 35, pets: ["Bobby"] },
{ firstName: "Bob", lastName: "Smith", age: 42, pets: [] },
{ firstName: "Charlie", lastName: "Brown", age: 28, pets: [] },
{ firstName: "Diana", lastName: "Prince", age: 30, pets: ["Bella", "Coco"] },
{ firstName: "Ethan", lastName: "Hunt", age: 45, pets: ["Buddy", "Rex"] },
{ firstName: "Fiona", lastName: "Gallagher", age: 27, pets: [] },
{ firstName: "George", lastName: "Clark", age: 39, pets: ["Oscar"] },
{ firstName: "Hannah", lastName: "Montana", age: 22, pets: ["Daisy", "Rocky", "Luna"] },
{ firstName: "Ian", lastName: "McKellen", age: 60, pets: [] },
{ firstName: "Jack", lastName: "Sparrow", age: 50, pets: ["Captain"] }
];
Now the use case will be, that we want to extract all the pets in a new array. This is indeed very simple and looks similar to the map function, but this time it is called flatMap.
const pets = customers.flatMap(customer => customer.pets);
console.log(pets);
> (10) ['Bobby', 'Bella', 'Coco', 'Buddy', 'Rex', 'Oscar', 'Daisy', 'Rocky', 'Luna', 'Captain']
And this is it, simple as that we have extracted all the pets into a new array.
Conclusion
The aforementioned HOFs are key in writing cleaner and expressive code. When you are faced with the decision of which one to chose follow the following rule of thumb:
- I want the same amount of elements but different, use map.
- I want less amount of element but same, use filter.
- I want a single value, use reduce.
- I want more elements and different, use flatMap.
And remember using functions that don’t mutate the original data.
I hope you enjoyed reading this article, keep it as a bookmark for a quick reference until you get familiar with those concepts and stay tuned for more JavaScript tutorials!