JavaScript map, filter and reduce functions explained, with examples
In this article I will explain, with examples, when and why you should use map
, filter
and reduce
in your JavaScript code. I will show you how these methods can make your code more declarative and readable, and also act as a gateway for getting into that functional programming mindset.
First, we will work through a simple example application, written with for
and for ... of
loops. We will then rewrite this example using filter
and map
, and then rewrite it again using reduce
. I will discuss the readability and performance of these solutions, and also mention some of the cases where you may actually be better off using for ... of
.
Finally, we will take a look at some of the use cases for the most complex of these 3 array methods,reduce
.
If after reading this article you would like to dive further into learning about JavaScript arrays, I recommend enroling on the Udemy course (affiliate link), Mastering JavaScript Arrays.
An example application: Albert Einstein quotes
We are going to build a very simple example application. It is a web page which fetches some famous quotes from an API endpoint, processes this data to extract the quotes of Albert Einstein, and then renders these in the DOM.
The full example is available on StackBlitz.
For the purpose of this section, we will be looking at one particular function from this StackBlitz example, getEinsteinQuotes
. The purpose of this function is to take an array of quote objects, which will be structured like this:
[
{
quoteText: 'I never think of the future. It comes soon enough.',
quoteAuthor: 'Albert Einstein',
},
{
quoteText: 'In the middle of every difficulty lies opportunity.',
quoteAuthor: 'Albert Einstein',
},
{
quoteText: 'Courage is going from failure to failure without losing enthusiasm.',
quoteAuthor: 'Winston Churchill'
}
]
And return an array of just the quoteText
of each Einstein quote, like this:
[
'I never think of the future. It comes soon enough.',
'In the middle of every difficulty lies opportunity.',
]
When iterating over arrays, many developers immediately reach for for
, forEach
or for ... of
. These constructs have their uses, but often result code which is imperative, verbose and difficult to read and reason about.
Here, we will first look at how to implement this getEinsteinQuotes
function using for
, and then for ... of
, before continuing on to see how we can implement the same function using map
, filter
and reduce
.
First attempt: for loop
First we will see how we can solve this problem using a for
loop:
/**
* Loop over the data with a for loop, use an if statement in
* order to push only the items we want to an array.
*/
function getEinsteinQuotes(quotes) {
const einsteinQuotes = [];
for (let i = 0; i < quotes.length; i++) {
const quote = quotes[i];
if (quote.quoteAuthor.includes('Einstein')) {
einsteinQuotes.push(quote.quoteText);
}
}
return einsteinQuotes;
}
This code certainly works, but it has its problems.
It is highly unreadable, and contains an unnecessary amount of code, both of which will lead to more mistakes and hence more bugs both during implementation and maintenance. In particular, much of the logic is concerned with low level details of working with arrays:
let i = 0; i < quotes.length; i++
is specifying how to handle the control flow of our for loop, ie. how to loop over the array.const einsteinQuotes = [];
, and latereinsteinQuotes.push(quote.quoteText);
is a solution for how to move items from one array into another.
Neither of these things are clearly related to the purpose of our function, our ‘business logic’. For a reader of the code they serve as ‘fuzz’ or obfuscation to quickly understanding what the function does.
Second attempt: for … of loop
Now let’s see how we can solve the same problem using a for … of loop.
/**
* Loop over the data with a for ... of loop, use an if statement
* in order to push only the items we want to an array.
*/
function getEinsteinQuotesList(quotes) {
const einsteinQuotes = [];
for (let quote of quotes) {
if (quote.quoteAuthor.includes('Einstein')) {
einsteinQuotes.push(quote.quoteText);
}
}
return einsteinQuotes;
}
This is slightly better than the for
loop method, but still not perfect.
The for ... of
loop iterates over each item in the array in order. Unlike with the for
loop, we do not have to manually write any logic for the control flow of the loop.
Third attempt: filter and map
Now for what I consider to be the best solution, using filter and map:
/**
* Transform the quotes data using filter and map.
*/
function getEinsteinQuotesList(quotes) {
return quotes
.filter(q => q.quoteAuthor.includes("Einstein"))
.map(q => q.quoteText);
}
Here we take a very different approach to either of the first 2 attempts.
-
filter
takes the quotes array and executes a predicate function (a function which returnstrue
orfalse
) for each item in the array. The function acts as a test to calculate which items should be filtered out.filter
then returns a new array containing all items for which the predicate function returnstrue
. -
map
takes the filtered quotes array and executes a callback function for each item in the array. The callback function’s job is simply to take a single array item and return a new value (in this case thequoteText
).map
returns a new array containing all of these mapped values.
Each of these functions returns an array, which is why they can be chained as they are above. Also, note how filter
and map
both take a function as a parameter - we call functions like this higher order functions.
The advantages of this approach are mostly with readability and conciseness:
- We have no need to explicitely create an empty array and append to it, our chained filter and map functions return an array of results and so so there is no longer a need for this.
filter
andmap
are very readable and declarative - the name of the method instantly tells us a lot the operation we are performing on the data (unlike thefor ... of
loop), and we do not need to manually write any control flow logic for iterating over the array, (unlike thefor
loop).
This type of solution is particularly favoured by those who are familiar with functional programming. This is because it relies on manipulating the data using pure functions, and does not mutate any state. Learning to use these JavaScript array methods can act as a gateway or introduction to learning functional programming.
Bonus attempt: reduce
Finally, lets look at how we can implement the same function using reduce:
/**
* Transform the quotes data using reduce.
*/
function getEinsteinQuotesList(quotes) {
return quotes.reduce(
(accumulator, q) => q.quoteAuthor.includes("Einstein") ? [ ...accumulator, q.quoteText ]: accumulator,
[]);
}
So this one takes a little longer to get your head around, hence I would generally recommend using filter
and map
as the optimum solution (for all but performance).
However, it is worth taking a look at, and noting that any operation which can be performed using a combination of map
and filter
can also be performed using a single reduce
function.
The idea is that reduce
iterates over an array, passing accumulated state from one iteration to the next - the final result is the return value of the final iteration.
In the above, the second argument is an empty array - this is the value passed as the accumulator
parameter for the first iteration. Each iteration then returns either just returns accumulator
if the quote is not an Einstein quote, or a new array containing the contents of accumulator
and the quoteText
of the current quote if it is an Einstein quote.
This is the hardest of these 3 functions to get your head around, but it has many interesting uses which I will talk about later in this article.
Performance
The for
loop and for ... of
loop (and forEach
function) are equivalent performance wise in modern JS engines.
map
, filter
and reduce
are generally a bit slower than the for
and for ... of
loops, especially when working with large data sets.
In our example, the main thing I would like to point out is how in our third attempt, we chained map
and filter
, resulting in the function iterating over each array element twice, and creating a new array twice (map
returns a new array, and so does filter
).
It is worth thinking about the fact that when you chain array methods like this, the array will be iterated over and a new array created for each method in the chain.
All in all attempt 3 is almost definitely the least performant, and yet generally it is still probably the best approach for most situations, especially if you are working with a small or medium sized data set. As a general rule, unless you strongly suspect or can show that the code you are working on is a performance bottleneck, it is better to write readable and maintainable code and refactor later on.
Should I ever use for
or for ... of
?
Yes, for ... of
has many good uses (I would personally always use for ... of
over for
).
Firstly, as discussed above for ... of
can be more performant in certain cases.
Secondly, for ... of
can and should be used for executing side effects, for example, a call to the DOM API. In fact, going back to our Einstein quotes example, lets take a look at the appendQuotesToDOM
function:
function appendQuotesToDOM(quotes) {
const ul = document.getElementById("quotes");
// Use a fragment to minise no of DOM reflows (check this)
const fragment = document.createDocumentFragment();
// Foreach is appropriate here, as this is a side effect.
for (let quote of quotes) {
const el = document.createElement("li");
el.appendChild(document.createTextNode(quote));
fragment.appendChild(el);
};
ul.appendChild(fragment);
}
Here we use a for ... of
loop in order to append each list item to the document fragment. In this case we are not transforming a data set, we are performing the side effect of appending to a DOM fragment - to use map
for this would be possible but not advisable because:
- By convention, the callback provided to
map
should be a pure function which ‘projects’ or ‘maps’ one value to another. - The callback passed to
map
returns a value each time it is called, andmap
will return an array of those values - but here, we would not have any need for this array of returned values.
All in all, if we were to use map
here, it would make our code less readable/reasonable, as we would be bending it to do something that most developers would not expect map
to be used for.
Further examples of reduce
While in our above example reduce
did not turn out to be the best solution, this does really not do it justice. reduce
is perhaps the most powerful of these 3 array methods and it has many interesting use cases. Lets look at a few:
Summing an array
Finding the sum of an array of numbers is one of the best examples to help you understand how reduce
works.
const sumArray = (arr) => arr.reduce((acc, n) => return acc + n);
const sum = sumArray([1,2,3,4,5]);
console.log(sum); // 15
Each iteration returns the running total. We could have provided 0
as the second argument to reduce
and it would have been used as an initial value, but we did not.
Because we did not, the first item in the array is skipped and instead gets passed to the acc
parameter for the first iteration. ie. for the first iteration acc
is 1
, n
is 2
.
Converting an array to a dictionary
This is an example of reduce
is a function which I have used in real projects quite a few times in the past. It converts an array of objects to a dictionary (ie. actually just a plain JS object) keyed by a specified property:
const createDictionary = (arr, key) => {
return arr.reduce((dict, obj) => {
dict[obj[key]] = obj;
return dict;
}, {});
}
const myArray = [
{ id: '174d0a85', name: 'Jimmy' },
{ id: '9b7e77e2', name: 'Sarah' },
]
const myDict = createDictionary(myArray, 'id');
console.log(myDict);
// {
// '174d0a85': { id: '174d0a85', name: 'Jimmy' },
// '9b7e77e2': { id: '9b7e77e2', name: 'Sarah' },
// }
Implementing pipe and compose functions
This is a great example of reduce
for those interested in functional programming.
In functional programming, we are often concerned with composing functions. For this, pipe
and compose
functions are often used.
If you are not familiar with functions like pipe
(or compose
) don’t worry, the purpose is essentially to pass any number of functions to pipe
(or compose
), and have it return a function which, when called, will pass the return value of each of these functions from one to the next, with pipe
finally returning the return value of the final function passed to it.
The only difference between pipe
and compose
is that pipe
calls the functions passed to it from left to right, while compose
calls the functions passed to it from right to left.
pipe
can be implemented using reduce
, and then used, as follows:
const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);
// The functions which we will compose using pipe.
const subtract1 = x => x - 1;
const square = x => x * x;
const add1 = x => x + 1;
const subtract1ThenSquareThenAdd1 = pipe(subtract1, square, add1);
const result = subtract1ThenSquareThenAdd1(6);
console.log(result); // 26
Here, it helps to think of our reduce as building as passing the results of the return value of one function call to the next in a nested fashion. ie. we can conceptualise our piped function as being (a function) equivalent to the following:
const subtract1ThenSquareThenAdd1 = x => add1(square(subtract1(x)));
compose
can be implemented using a very similar JavaScript array method, reduceRight. This method is exactly the same as reduce
, except that it iterates through the array backwards:
const compose = (...fns) => x => fns.reduceRight((v, f) => f(v), x);
// The functions which we will compose.
const subtract1 = x => x - 1;
const square = x => x * x;
const add1 = x => x + 1;
const add1ThenSquareThenSubtract1 = compose(subtract1, square, add1)
const result = add1ThenSquareThenSubtract1(6);
console.log(result); // 48
compose
does exactly the same as pipe
, but calls the functions pass to it from right to left. We can conceptualise our composed function as being (a function) equivalent to the following:
const subtract1ThenSquareThenAdd1 = x => subtract1(square(add1(x)));
Sequentially resolving promises
reduce
can be used to chain promises so that they execute sequentially. Lets look at a simple example where we fetch data from a number of API endpoints:
const urls = [
'https://some-api.com/endpoint1',
'https://some-api.com/endpoint2',
'https://some-api.com/endpoint3',
];
urls.reduce((previousPromise, url) => {
return previousPromise.then(() => {
return fetch(url);
});
}, Promise.resolve());
Each of these requests will be made sequentially - the next one being made when the previous one has received a response.
If we unwrap this mentally, we can think it as essentially building a promise chain using then
. And if you are familiar with promises, of course each then
is executed only once the previous one has resolved. reduce
creates a chain of promises equivalent to this:
fetch(() => urls[0]).then(() => fetch(urls[1])).then(() => fetch(urls[2]));
Alternatively, using modern async
and await
syntax:
const urls = [
'https://some-api.com/endpoint1',
'https://some-api.com/endpoint2',
'https://some-api.com/endpoint3',
];
urls.reduce(async (previousPromise, url) => {
await previousPromise;
return fetch(url);
}, Promise.resolve());
Which we can think of as equivalent to:
await fetch(urls[0]);
await fetch(urls[1]);
await fetch(urls[2]);
Conclusion
In this article we looked at some examples of how and when to use map
, filter
and reduce
, and hopefully this has given you some further insight into how these functions work.
Now that you understand these 3 methods, there are many other useful array methods you can learn such as some, every, includes
and find
. And for more advanced data manipulation with arrays there are libraries such as lodash, or for a more functional approach, ramda.
Useful Resources
If you are looking to learn about JavaScript arrays in more depth, I recommend the following course (affiliate link):
In addition, the following were really helpful for writing this article, and I very much recommend them as further reading: