.map(), .reduce(), .filter() - my turn

A while back I written an article about some interesting array's functionalities - with .map(), .reduce() and .filter() being only a small fraction of them. But, because of the number of different methods involved, some of them simply didn't get the attention they deserve. These include the 3 mentioned above. Why I'm talking about this? Well, mostly because I've seen a lot of content about JS arrays lately - about these 3 methods to be specific. Mostly beginner-friendly, describing how to make your code better in terms of both readability and convenience, only by utilizing these methods. And, as this is mostly a beginner-friendly blog you're reading - it's my turn now! So, bear with me for a moment, as we dig into these 3 array methods deeper than ever!

Reasoning

First, let's talk about why you'd even want to use array methods, instead of e.g. simple loops? What are their pros and cons?

As you may know, JS arrays present a great way of representing collective data. Since ES6, they're considered to implement iterable and iterator protocols. What it basically means, ist that you can easily iterate over their values using standard loops.

const values = [0, 1, 2, 3, 4];

for(let i = 0; i < values.length; i++) {
    values[i];
}

for(const value of values) {
    value;
}

for(const index in values) {
    values[index];
}

Back in the times when ES5 was a thing, the best way of iterating an array was with the standard for loop. But, as you can clearly see in the example above, it brings some additional boilerplate to the table. With ES6 and its for... of loops, the situation has changed. Yet still, there are some use-cases which require additional variables or even multiple loops! But, these are fairly simple, and tempting to look for "clearer" alternatives. And that's what methods like .map() allow you to do!

Arrays have a lot of methods at their disposal, specifically ones to simplify the iteration process in different use-cases. You know, just some simple and nice shortcuts for different kinds of loops. But, they're not only that! Some of them represent obvious examples of the influence of functional programming (FP) paradigm inside JS.

Is .map() a function? - Well, it's a method, so only somewhat close. Does it change the general state of the program? - Only if you make it do so (which you shouldn't BTW). Is the input data mutated in the process? - Not at all. As you can see, the general ideas of FP are definitely here (at least to some degree), with immutability being the most important one. Not changing (mutating) the input data is considered one of the main features of all .map(), .reduce() and .filter(), which I'd like to talk about a bit more...

While immutability in all 3 methods we're talking about makes sense out-of-the-box, which we'll talk about in a minute, there are whole libraries, written only to satisfy the rules of FP. And, in such scenarios, not everything is so straight-forward. Here, we'd need to take a look at the more general advantages of immutability. These are improved readability, efficiency and data history perseverance. You must agree - it's much easier to keep track of your code, when every time something has to change, its simply created from ground-up, based on already existing data. You don't have to investigate the code line by line to find out what's going on. Creating new data instead of mutating the old one is also (somewhat) more efficient. In the memory underneath all abstraction layers, data is only needed to be written, instead of accessing it first, which is naturally faster. Also, having access to all data whether its old or new, present a possibility for a super-easy way of saving its previous states. Something that's extremely useful for debugging, backups and other kinds of applications.

With all that good stuff, are there any cons to immutable data? Well, there's only one I can think of right away, and that's performance. And that's even when modern JS engines are becoming increasingly good at dealing with immutable data. With mentioned efficiency when saving new values and improved garbage-collector, it all comes together for better immutability. So, why would performance suffer? Naturally, with more and more data being created in the process (which still isn't that efficient), a slowdown will finally occur. But, as it's not that significant, and the benefits clearly outweigh any possible performance losses - it's worth it! The only exceptions to this rule are demanding apps and games, where every last bit of performance counts.

.map()

Whoa, so now we know that there's clearly something more to those array methods than meets the eye. Let's explore all 3 of them, starting with .map()!

Take a look at this little snippet:

const values = [0, 1, 2, 3, 4];

function map(arr, callback) {
    const mappedArr = [];
    
    for(let i = 0; i < arr.length; i++) {
        mappedArr.push(callback(arr[i], i, arr));
    }
    
    return mappedArr;
}

const doubledValues = map(values, value => value * 2); // [0, 2, 4, 6, 8]

And this one too!

// ...
const doubledValues = values.map(value => value * 2); // [0, 2, 4, 6, 8]

See the resemblance? Both of these code snippets are meant to do the same thing - just double all the values form the input array and return a new one. First, we're doing it with our own mapping function. It's quite simple, nothing special. There's even no error handling included! Yet still, the function alone requires at least a few lines of code. Thankfully we have the same functionality provided to us by .map()!

So, is there any performance difference between these two?

http://jsbench.github.io/#1b9b8256dc38e0749c1552a3ddaa5305

Kudos for .map()! And it doesn't even include function definition! Would mutating our input array improve the performance?

http://jsbench.github.io/#9b07d7657244fdf18a40308673ee1721

Doesn't seem any different.

Now, the point I'm trying to make here is not just throwing random benchmarks at your face and make you look at them, but rather show that you have nothing to fear (in terms of performance) when using array methods. The performance difference is neglectable at worst, and even a magnitude better than any alternatives at its best (as demonstrated)! So, if you had any concerns about it - here's your answer!

Let's get back to .map() though. As you've it has a nice and simple syntax. Just use one of your arrays and simply invoke it! It takes only one argument, usually referenced to as a callback. Like our loop-based implementation above demonstrated, it's run once for every element in an array, can take 1 up to 3 parameters - current element's value, its index, and the original input array - and should return the new value, for the respective array's element.

// ...
values.map((value, index, arr) => {
     // ...
});

Also, I think no one would argue, that because of the arrow function syntax, the .map() is looking even better!

As the syntax is so simple, let's discuss the possible use-cases now. Definitely, there must be a ton, if the method is included in JS itself, right? - Right. The most common one, as shown by React, is a conversion of data from one form to another. In React's case - from almost any type of data to specific React elements. So, yeah, think of React, boards, data collections conversions and all that kind of stuff...

.reduce()

What's amazing about all 3 methods we cover is how intuitive they are. Just look at their names - .map() is used for data "mapping", .filter() for filtering it, and .reduce() for reducing multiple values to a single one.

Now, .reduce() is one of my "favorite" methods. It may sound kind of weird but I really enjoy its simplicity and how it works in general, even though I don't use it as much as e.g. .map(). Anyway, what can it do?

// ...
const sum = values.reduce((sum, value) => sum + value, 0); // 10

.reduce() is extremely useful when e.g. summing numbers and stuff. Reducing multiple values to one is the most useful when used with simple types of data like numbers, strings or even booleans. But, if you're using it with any other kinds of data, unless these are collection-like, e.g. arrays, objects (better use Object.assign()), you most likely either know what you're doing or should consider using a different method. Keep in mind that .reduce() isn't meant for usual iteration and was designed with specific use-cases in mind.

As for performance - you know how that goes. Let's instead discuss the syntax of .reduce() method, as it's a bit more complex than the one we know from  .map(). At its core .reduce() takes two arguments - a reducer function, used to reducing array's values into one, and the second one for setting up the "accumulated" value.

Reducer function is run at every element of the input array. It provides you with 4 parameters - accumulated value, and all the array-related stuff that .map() has. Then, it should return the newly accumulated value. Let me showcase it a bit better with an alternative implementation of .reduce() method.

// ...
function reduce(arr, reducer, initialValue) {
    let accumulatedValue = initialValue;
    
    for(let i = 0; i < arr.length; i++) {
        accumulatedValue = reducer(
            accumulatedValue,
            arr[i],
            i,
            arr
        );
    }
    
    return accumulatedValue;
}

const sum = reduce(values, (sum, value) => sum + value, 0); // 10

Here, we're once again using the standard for loop, to have easy access to the currently iterated element's value, index and the input array - just like with standard .reduce() method. I think the above snippet illustrates everything you need to know in a clean and approachable way.

.filter()

Two down, one to go. Let's talk about .filter()! You know - the one for filtering stuff!

Filtering data is a very, very common process of removing unneeded or unwanted values from the given collection. And, as arrays are the collections in JS, it's unsurprising that they come with .filter() method built-in.

// ...
const filtered = values.filter(value => value > 2); // [3, 4]

Although .filter() can seem like any other method discussed in this post - it has an intuitive name, and it does its own job - I consider somewhat special. When you think of it, .filter() is the only method in our little 3-item-long list, that could possibly be mutable. .map() is meant literally change the whole array (map it to something different), and .reduce() - only to return a single value, thus they can't even possibly be "mutable". But, .filter() technically should only filter out the values that you want it to. That's why I consider the immutability here as an additional feature, which, strangely enough, I take for granted. I think immutable .filter() is just better form standardization standpoint. Memorizing and using the API is much easier when methods are designed and work in a similar fashion.

// ...
function filter(arr, callback) {
    const filtered = [];
    
    for(let i = 0; i < arr.length; i++) {
        if(callback(arr[i], i, arr)) {
            filtered.push(arr[i]);
        }
    }
    
    return filtered;
}

filter(values, value => value > 2); // [3, 4]

.filter() is fairly simple. It's based on the passed callback function, which should do the filtering. It's run on every element, and - provided element's value, index and the input array - should return a boolean, indicating whether the element should be preserved in the result array - the usual stuff.

What's unusual about .filter() is it's second argument. That's right - there is one! For whatever reason, you can specify the this value for use when calling the callback function. I didn't include it in my little implementation, because I consider it kind-of pointless. In the era of arrow functions - what exactly you'd like to bind? Like, seriously, messing with this only brings additional and unwanted complexity to the code. So, if you don't have to use it (which is the case in 99.9% or 100% I guess), please - don't.

Chaining

You've probably already noticed by now, that the use of any of the mentioned methods, can result in some pretty-looking one-liners, especially when used with arrow functions. But, do you know what this pretty look can give you when combined with immutability? That's right - chaining!

const values = [0, 1, 2, 3, 4];

const processed = values
    .map(value => value * 2)
    .filter(value => value > 2)
    .reduce((sum, value) => sum + value); // 18

That's kind of a creative, isn't it? While you don't necessarily have to chain these methods, in this order specifically, you should at least know, that you can. I think this makes the code look nice and readable. And, when combined with other, immutable array method - who knows? Maybe you can even create something actually useful with it?

End of conversation

I must admit I didn't know that so much can be said, only about these 3 array methods! I'm truly impressed! The world of programming is so vast, and so much can be said even about its tiniest elements. Anyway, that was my deep-dive take on .map(), .reduce() and .filter(). I hope you enjoyed it and learned something new / reminded yourself of some things. Let me know in the comments and reactions section how I did. If you like this article, consider following me on Twitter, on my Facebook page or through my weekly newsletter for more JS content! And, as always, have a great day!