The Array.reduce Method Part 2 - Replacing Map and Filter Chains
Greetings, friends! This is Part 2 of my tutorial series on the Array.reduce method in JavaScript! In this tutorial, I will discuss how to rely less on the Array.map and Array.filter methods. We won't be replacing them entirely, but we will rely less on chaining them.
Method Chaining
In JavaScript, we can chain certain methods together. Let's see an example.
const array = [-1, 1, -2, 2, -3, 3];
const newArray = array.filter(num => num > 0).map(num => num * 2);
console.log(newArray); // [2, 4, 6]
In the example above, we are chaining together a map
method and filter
method. The filter
method is removing all negative numbers and returning a new array. The map
method then doubles every element in this new array and returns it.
We could have also declared functions and passed them into the map
and filter
methods instead of using arrow functions.
function removeNegatives(num) {
return num > 0;
}
function doubleNumbers(num) {
return num * 2;
}
const array = [-1, 1, -2, 2, -3, 3];
const newArray = array.filter(removeNegatives).map(doubleNumbers);
console.log(newArray); // [2, 4, 6]
Note that method chaining is possible because the filter
method returns an array, and the map
method needs an array to work. You can't chain map
or filter
after forEach
because forEach
returns undefined
.
const array = [1, 2, 3];
array.forEach(num => num * 2).map(num => num * 2);
/* ERROR:
Uncaught TypeError: Cannot read properties of undefined (reading 'map')
*/
Replacing Map and Filter Chains with Reduce
Let's think about what happens we chain together map
and filter
methods.
// Iterates through six elements
const filteredArray = [-1, 1, -2, 2, -3, 3].filter(num => num > 0);
console.log(filteredArray); // [1, 2, 3]
The filter
method will iterate through the entire array once. Then, we call the map
method to double every element.
// Iterates through three elements
const finalArray = filteredArray.map(num => num * 2);
console.log(finalArray); // [2, 4, 6]
We had 6 iterations when applying the filter
method and 3 iterations when we applied the map
method for a total of 9 iterations. This is also the optimal order of operations. Imagine if we did the map
method first followed by the filter
method.
// Iterates through six elements and returns an array of six elements
const doubledArray = [-1, 1, -2, 2, -3, 3].map(num => num * 2); // [-2, 2, -4, 4, -6, 6]
// Iterates through six elements again and returns an array of three elements
const finalArray = doubledArray.filter(num => num > 0); // [2, 4, 6]
That would have been a total of 12 iterations! This doesn't seem like much, but imagine if we had hundreds or thousands of elements in an array! Wouldn't it be nice to iterate through the original array just once? Then, we'd always be iterating through an array equal to the number of elements in that array.
The reduce
method to the rescue! Let's replace a .filter.map
chain with one call to the reduce
method instead.
const array = [-1, 1, -2, 2, -3, 3];
const initialValue = [];
// Iterates through six elements
const newArray = array.reduce((accumulator, currentValue) => {
if (currentValue > 0) {
const doubled = currentValue * 2;
accumulator.push(doubled);
}
return accumulator;
}, initialValue);
console.log(newArray); // [2, 4, 6]
Let's dive into what's happening in the code snippet above. The most important thing to notice is that we are passing in initialValue
as the second parameter of the reduce
method. This value is set to an empty array, which means the accumulator will be equal to an empty array during the first iteration of the reduce
method.
Now, let's look at the reducer, the callback function we pass into the reducer
method. We check if currentValue
is greater than zero. During the first iteration of the reducer
method, currentValue
will be equal to -1
. Therefore, the if
condition fails and we simply return the accumulator
which is still set equal to an empty array.
During the second iteration, currentValue
is now equal to 1
, which is greater than 0
. Therefore, we double this value to 2
and push this number to accumulator
. Now, accumulator
is no longer an empty array and contains one value.
This pattern continues until we have iterated through the entire array. The final value stored in accumulator
will be an array that contains [2, 4, 6]
, which is the same array we got when we chained together the filter
and map
methods in earlier examples.
Here is a tabular breakdown of what happens during each iteration of the reduce
method:
iteration | accumulator | current value | return value |
---|---|---|---|
1st | -1 | ||
2nd | 1 | 2 | |
3rd | 2 | -2 | 2 |
4th | 2 | 2 | 2, 4 |
5th | 2, 4 | -3 | 2, 4 |
6th | 2, 4 | 3 | 2, 4, 6 |
Instead of iterating 9 times by calling Array.filter.map
in our earlier example, we only had to iterate 6 times, or the length of the original array. This may not seem like much, but scale up the array to hundred or a thousand times, and you'll surely notice a big difference in performance.
Conclusion
I hope you understand why learning about the reduce
method is so crucial. We can drastically improve the performance of our code by rewriting .filter.map
method chains using the reduce
method. The reduce
method is very powerful, but it does require reconstructing our logic a bit. This method isn't limited to JavaScript either. Multiple programming languages such as Ruby and C# implement their own version of the reduce
method. Learn how to create powerful reducers and you can reduce the time it takes your code to run 😉.