I see a lot of questionable blog posts and articles out there about Javascript. And, I should know, I have written some of them myself in the ten years I’ve been blogging on this site.
However, over the last few years since the rise of Medium and Dev.to, I have noticed a few articles (I won’t link or name any) which go along the lines of this:
Stop Using For Loops and Use map, filter and reduce instead
Loops Are Dead, Use These Functional Methods Instead
You get the idea.
Some people advocate for using these functional and chainable methods in place of for loops and in some cases, they are right. But, to say that all for loops can and should be replaced, it is insanity.
A quick refresher on the three functional methods that the Javascript functional crowd pray to before they fall asleep at night and when they wake up in the morning.
.filter()
Say you have a list of users who have ages. You want an array only containing users over the age of 20 and under the age of 31, using filter you can specify what items get discarded and what items get kept. You then get a new array containing your new users.
const newUsers = users.filter(user => user.age >= 20 && user.age < 31);
.map()
This is a useful method for transforming data. If you have one or more products coming back from the API and you want to normalise some of the property names or even make the product objects small, map is a good way to change the shape.
const newProducts = products.map(product => ({ id: product.id, name: product.name, price: product.price }));
.reduce()
This powerful method allows you to take data and change its structure completely. If you take the theoretical list of users from the filter
section above and you wanted to know the combined age total of your users, you can use reduce
to do that.
const ages = users.reduce((accumulator, user) => accumulator + user.age, 0);
Reduce works on the concept of an accumulator which can be a running total, it could be an array, anything. The value of zero above is the initial value. If you were working with an array, it would be []
and so on.
The best feature about these methods is they can be chained together. You can filter, map and reduce to your hearts content. They are effective methods and when used correctly, can make your code cleaner and easier to test as well.
However, they don’t work in all situations. Perhaps one of the most notable examples of where these functions fall down is when dealing with asynchronous code, more specifically: promises and generators.
If you’re working with async/await
and inside of your loop you’re making an API request or anything else involving a promise, you want to wait for it to resolve before continuing to the next element in your array (or whatever you’re looping over).
The reality is, filter
, map
and reduce
fall down if you need to await anything. Without some clever hacks and extra code, they will not work with these methods. This is where the old trusty for loop comes in handy and there is no better for loop than a for..of
loop.
for (const item of items) { item.meta = await getItemMetadata(item.id); }
This is probably a poor example of using await inside of a for loop, but you get the idea. Don’t roast me too hard for using a for loop like a map
here. For loops can work with asynchronous code and there will come a time when you might encounter such a use-case.
Async Iteration
Even better, async iteration which landed in ES2018 takes this concept a step further and use for await
at the top-level of the loop. This is known as a for..await..of loop.
for await (const user of getUsers()) { }
The beautiful thing about this is that it will work with async iterable objects as well as sync iterables, you don’t just have to use promises or generators to use for..await..of
.
Another example of where a for loop is more appropriate is when you’re iterating over a collection but you’re not doing anything with the data. Maybe you’re calling a function.
for (const item of items) { markItemForCompletion(item); }
Sure, you could use filter
, map
or reduce
but you’re not filtering anything, you’re not changing the structure of the data and you’re not reducing it down to anything, you just want the items inside and to call a function.
Another and perhaps a more fitting example is displaying data. I’ve seen a lot of React developers abuse the map
method just to display a list of data. The proper use case when displaying data would be to use a for loop once again. You’re not changing anything, you’re just iterating over the items and displaying them, keeping their shape and structure intact.
Conclusion
You can get quite far using filter
, map
and reduce
— no question about that. I believe you should use them until you can’t.
If you are after some more robust examples of what happens when you try using async/await
inside of the aforementioned methods, this blog post by Zell goes into a little more detail and has some great examples. It’s possible to get something working, but there are too many pitfalls.
You also might have noticed throughout this article, I barely mentioned forEach
and, I didn’t show any examples of it. I am of the opinion that for..of loops are better than forEach
not only in performance, but their support for control flow as well as the ability to work with all iterables without the need to coerce values.
But, don’t go using them in instances where they
A) do not work
or
B) are not applicable just for the sake of being a purist or because someone on Medium told you too.
P.S. No, the irony that I call out blog posts and content telling people how they should do things in Javascript and then proceed to unload my opinions on you was not lost and could not be helped.
Hi Dwayne,
I agree with your statement about blindly following advice.
People just need to be aware that forEach is not Promise aware.
Having a “For await of” (https://tc39.es/ecma262/#sec-for-in-and-for-of-statements) will be nice, too.
Regards
While I don’t have anything against for loops, you have to admit items.forEach(markItemForCompletion) is both succinct and clear. The example would be more compelling if you were accumulating multiple things across calls or using the index to construct unique names.