How to use map, filter, and reduce in JavaScript
How to explain the functions map
, filter
, and reduce
?
Why even focus on these functions? Why do these specifically need explaining?
Let’s start with that last question.
Why map, filter, and reduce?
These functions are a fundamental part of working with data. And when programming, you’re basically juggling data all the time.
Specifically, arrays of data.
You’re transforming an array of items into another array of items.
Or you might be selecting items from an array that meet certain criteria.
This happens so often in programming, that it’s nice to have functions specifically for this purpose.
Of course, as with all problems in programming, there are a million ways to solve these.
One very common way that beginners often get taught to do this is by using loops.
For example, you could write a for
loop to iterate over a list of items, and do whatever you want with each item:
for (var i = 0; i < items.length; i++) {
var item = items[i];
// do something with the item: transform it, remove it, etc.
}
This is such a classic programming example, I could write it in my dreams.
And yet, I never write it in my waking hours.
Why not? Because map
, filter
, and reduce
give me a very expressive handle to reason about these problems.
With handle I mean a name to describe the problem.
With these functions on hand, I have a shared vocabulary to share my intent with my colleagues.
I can say: “I’m mapping this list from X to Y”, and they will know what I mean.
The for
loop is very anonymous. I have to inspect the contents of the loop block to understand what’s happening, whereas a map
or filter
call immediately expressesd the purpose of that piece of code.
Furthermore, the loop requires more code to do the same thing.
For instance, in order to create a new array with only selected items, I will have to create that array beforehand, and add an item to it on every iteration.
To my mind, these are a lot of moving parts that I need to keep in my head.
As you will see, when mapping and filtering, all of that is implicit.
This tutorial will try to explain these functions to you, to allow you to use and recognize these same handles and benefit from their expressive power.
💡 Disclaimer
In functional programming and Category Theory, these functions are part of a larger set of algebraic data structures, like Functor
, Applicative
, and Monad
.
I am aware of this, and if you are too, you are not the intended audience for this article! This article is aimed at people unfamiliar with these concepts, and who want to get a pragmatic handle on these functions in their everyday programming life. Therefore I will skip this nuance, and will point people to Professor Frisby’s Mostly Adequate Guide To Functional Programming if they wish to explore further. ❤️
Map
Mapping is explained as transforming data from one shape to another. Let’s visualize it like this:
[🍎, 🍎, 🍎, 🍎, 🍎] => [🍐, 🍐, 🍐, 🍐, 🍐]
An important characteristic of mapping an array is that the resulting array will have the same length as the original.
In other words: you cannot add or remove items from the array using map
.
The transformation is defined by the function you pass to map
.
This function is then applied to every item in the list.
This silly example will transform every apple into a pear:
const apples = ["🍎", "🍎", "🍎", "🍎", "🍎"];
const pears = apples.map((apple) => {
return "🍐";
});
Another example could be to add taxes to a list of prices:
const prices = [10, 20, 30, 40, 50];
const taxedPrices = prices.map((price) => {
return price * 1.21;
});
In this case we’ve used the function’s parameter to calculate the new value.
Just as in our for
loop example, map
will go over every item in the list, and execute the callback function. It passes the current item into the function as a parameter, and whatever is returned from the function is added to a new list.
👉 Quick sidebar about immutability
Those last two words are pretty important: map
, filter
and reduce
all return a new list.
This means that in the above example, prices
is still [10, 20, 30, 40, 50]
.
taxedPrices
is a completely separate list, existing at the same time next to prices
.
For instance, in an e-commerce product, that means I can show prices
in the product listing, but taxedPrices
in the shopping cart. The fact that I changed the prices to create taxedPrices
doesn’t affect the original list at all and I can still use their original values in my interface.
This is another big advantage of using these functions over loops: even though critics will tell you arrays in Javascript are not completely immutable, these functions do promote an immutable style of programming. This is nice because data is not being changed left and right, but copies of the data are made, before performing the change.
Because it’s a lot easier to see where changes are happening, your code becomes easier to debug.
👈 Back to the tutorial!
In conclusion:
map
takes a list, and gives you a new list of the same length, where every item has been transformed by a callback function.
Filter
Filtering is explained as selecting items from a list that meet certain criteria. Let’s visualize it like this:
[🍎, 🍐, 🍎, 🍐, 🍎] => [🍎, 🍎, 🍎]
As you can see, the resulting array can be shorter than the original array.
In fact, making an array shorter than the original is often the exact purpose of filtering!
The criteria for selecting items is defined by the function you pass to filter
:
const fruits = ["🍎", "🍐", "🍎", "🍐", "🍎"];
const apples = fruits.filter((fruit) => {
return fruit === "🍎";
});
Just like map
, filter
will go over every item in the list, and execute the callback function.
Every time the function returns a truthy value, the item is added to a new list.
In the example above, the callback function is:
(fruit) => {
return fruit === "🍎";
};
This function will return true
if and only if the given fruit is an apple.
That means the resulting new array will contain only apples.
In conclusion:
filter
takes a list, and gives you a new list of the same or shorter length, containing every item that meets the criteria defined by a callback function.
Reduce
Oof. Now we’re getting into the weeds.reduce
is a bit of a wildcard, and it’s the hardest one to explain, because it’s the most versatile.
Let’s visualize it like this:
[🍎, 🍐, 🍎, 🍐, 🍎] => 🍷
reduce
can completely change the shape of the data. That means that even though the input is always an array, the output can be whatever you imagine! It could be another array, but it could also be a number, a boolean, a Date object, or anything else.
When is that applicable?
- When you want to transform a list into a single value.
For instance when calculating the sum of a list of numbers:
[8, 10, 5, 19] => 42
- When you want to increase the length of an array.
For example, when you want to flatten a list of lists:
[[🍎], [🍐, 🍐], [🍌]] => [🍎, 🍐, 🍐, 🍌]
Or in any other case where map
and filter
are simply not applicable.
reduce
is sometimes criticized for being hard to understand, and for being hard to read.
Frankly, that’s fair. Looking at the average reduce
function requires you to keep just as much moving parts in your head as in the for
loop I critized at the beginning of this article!
Still, it’s worth understanding reduce
, and you can be the judge of when to use it in your projects.
And of course gaining some experience with it will slowly make it more familiar.
Let’s look at the sum example:
const numbers = [8, 10, 5, 19];
const sum = numbers.reduce((total, current) => {
return total + current;
}, 0);
As you can see, reduce
itself takes two arguments: the callback function, and an initial value.
In the example above, 0
is the initial value, and the callback function is defined as:
(total, current) => {
return total + current;
};
As you can see, the callback function also receives two parameters: total
and current
.
This is, in my opinion, where much of the confusion comes from. current
is easy: it’s the current item in the list, in the same way as map
and filter
pass every item in the list to the function.
But where does total
come from?
The answer is: total
is the result of the previous iteration of the callback function. The initial value is used in the first iteration, when no previous iterations have been performed yet.
Let’s illustrate this for our sum example, to drive home the point:
const numbers = [8, 10, 5, 19];
const sum = numbers.reduce((total, current) => {
return total + current;
}, 0);
- First iteration,
total
is equal to0
, since that’s the initial value.current
is8
, as it’s the first item in our list. - Second iteration,
total
is equal to8
, since that’s the result of the previous iteration (0 + 8
).current
is10
, the second item in the list. - Third iteration,
total
is equal to18
, since that’s the result of the previous iteration (8 + 10
).current
is5
, the third item in the list. - Fourth iteration,
total
is equal to23
, since that’s the result of the previous iteration (18 + 5
).current
is19
, the fourth and last item in the list. reduce
is done, there are no more items in the list, and the last iteration returned23 + 19
, meaning the resulting value,sum
, is equal to42
.
Phew! As you can see, reduce
is a bit of a handful.
Where map
and filter
are well-defined handles that, with some experience, you can internalize and use without much thought, reduce
is a bit more of a puzzle, mostly because its result is custom to your needs every time.
In conclusion:
reduce
takes a list, and gives you a new value, that might or might not be a new list.
Iteration for the sake of iteration
I sometimes see beginners use map
purely for its iterative nature.
For example, when you want to perform an action for every item in a list, but you don’t care about the result, you can use map
and simply ignore the resulting list:
const items = [1, 2, 3, 4, 5];
items.map((item) => localStorage.setItem(`item-${item}`, item));
While this is valid JavaScript code, you will lose some of the expressive power of these functions. What happens above is what in functional programming is called a side effect. Meaning it triggers an effect elsewhere, outside the scope of the function.
In this case the effect is that it stores items in localStorage
.
What I like about map
is the fact that it returns a new list.
Assignment, in other words storing a value in a variable, is part of the reason to use map
in the first place.
In our example, where we’re only interested in the side effect, I would prefer to use forEach
:
const items = [1, 2, 3, 4, 5];
items.forEach((item) => localStorage.setItem(`item-${item}`, item));
forEach
is nothing more than an iterator. It returns nothing. Using forEach
therefore clearly signals that we’re not interested in the result of the iteration, but only in the side effect.
Side effects are by definition a bit harder to reason about, so it’s very nice to have a bit of code signal to us clearly that the intent is to perform a side effect!
Again, much of this is motivated by a desire to write code that explains itself through the concepts captured in the names used.
Concluding
I hope you have a good grasp on these three functions now, with a clear idea of when to use which.
In conclusion, I would like to admit that there are more parameters to JavaScript’s native map
, filter
and reduce
functions. I’ve omitted these here because they are not essential to understanding the core concept. But please take a look at the following MDN pages to get a more complete picture:
Obviously there are a lot more functions in the JavaScript standard library that are worth exploring, but these three functions are at the core of them all, so learning them yields the most benefit.
In fact, a fun experiment could be to see if you can implement functions like find
, some
, every
, flatMap
, and sort
using only map
, filter
, and reduce
.
Good luck, and have fun!