Every once in a while when I do code review or visit StackOverflow I stumble upon code snippets that look like this:
const fruitIds = ['apple', 'oragne', 'banana'];
fruitIds.map((id) => {
document.getElementById(`fruit-${id}`).classList.add('active');
});
So as you can see it's just a simple iteration where for every element in the fruitIds
array we add active
class to a certain HTML element in a DOM.
Many programmers (especially new) wouldn't notice anything wrong with the code above. However, there is a one major issue here - the usage of .map()
. Let me explain.
What is wrong with .map()
?
Well, there is completely nothing wrong with this particular array method. In fact, I think it is very handy and beautifully wraps one of the iteration patterns - mapping.
In simple words, mapping is an operation which applies a function to every element of a collection and returns a new collection with elements changed by the mentioned function. For example, if we have an array of numbers const nums = [1, 2, 3, 4];
and would like to receive a new array of doubled numbers, we could map the original array to a new one like this (in JavaScript):
const biggerNums = nums.map((n) => n * 2);
// >> [2, 4, 6, 8];
The biggerNums
array would consist of numbers from the original nums
array multiplied by 2
.
Notice how .map()
is used - we assigned the result of this method to a new variable called biggerNums
. I have also mentioned earlier that mapping is an operation that returns a new collection of elements. And this is the very reason the code snippet showed at the beginning of this article is wrong. The .map()
returns a new array - always - and if we don't need that array, we shouldn't use .map()
in the first place. In this particular case (simple iteration) a different array method should be used - .forEach()
- which is specifically designed for such cases. It doesn't return a new collection, it simply walks through an array and invokes a callback function for every element allowing you to do something for each of them.
So the correct version of the mentioned snippet should look like this:
// good way
const fruitIds = ['apple', 'oragne', 'banana'];
fruitIds.forEach((id) => {
document.getElementById(`fruit-${id}`).classList.add('active');
});
We don't need a new array so we simply iterate over the fruitIds
array and add the active
class to an HTML element for each of the array items.
Okay, but why should I care? .map()
is shorter and easier to write than .forEach()
. What could possibly go wrong?
Consequences of abusing .map()
One of the worst consequences of abusing .map()
is the fact that it returns a new redundant array. To be more specific - it returns a new array of the same size as the one this method was called on. It means that if we have an array of 1000 elements, .map()
will return a new array of 1000 elements - every time.
In JavaScript, all functions return a value. Even if we don't use the return
keyword, the function will return undefined
implicitly. That's how the language has been designed. This rule also applies to callbacks - they are functions too.
Having said that, let's get back to the original example:
// wrong way
const fruitIds = ['apple', 'oragne', 'banana'];
fruitIds.map((id) => {
document.getElementById(`fruit-${id}`).classList.add('active');
});
What happens here? An array of fruit IDs is created and then it's mapped to another array of the same size. Even though the array returned by .map()
is not used, it does take place in memory. This new (unused) array looks like this:
[undefined, undefined, undefined]
It's because the callback passed to the .map()
method does not have the return
keyword and as we know, if there is no return
, undefined
is returned implicitly.
How bad is it? Very bad. In this particular example it won't bring any serious consequences - there are only three items in the array so creating another three-element array won't cause any problems. However, the problem arises when we deal with big arrays of complex data. If we want to iterate over an array of five thousand objects and we abuse .map()
, we create another array of five thousand elements - undefined
s. So we end up storing 10 000 elements in memory from which a whole half is redundant. It is a very non-optimal practice and in some scenarios it may even lead to the application overload. This is why we should pick right methods to the right tasks.
Summary
There are many practices that are essentialy bad, but the negative consequences will start to be visible only when dealing with bigger datasets. One of such practices is abuse of .map()
. When operating on small arrays, it won't cause any hurt. But when we make this mistake with a bigger array it will start overloading our application and it may be quite hard to debug.
This is why we should never let it pass by and whenever we see this abuse, we should take care of it. I hope now you understand why.
Latest comments (47)
Nice article will make sure to use
forEach
instead ofmap
wherever necessaryYeah I know, but Object.keys, Object.whatever with different results with unknown objects (if no ts)
the article would have been complete with a bechmark.
Here we are jsbench.me/ujkub8rcv2/1
forEach
seems to be the fastest in that snipped.map
seems to be around 20% slower andfor..of
15% slower.Don't worry, it will make your code smelly only. Memory will not be an issue.
The day we have map() working for objects it'll be everywhere :')
I guess incorrect use of Map is really only an issue if it is harming the app performance. Someone dealing with huge datasets will see that. Otherwise the returned array is going to be garbage collected after the containing function exits.
Still, better to use the right tools. And you know, there is a lot of docs for that!
developer.mozilla.org/en-US/docs/W...
It is good to know many of these. I mean, who uses copyWithin()? I mean, handy, but I haven't needed that edge case myself. includes() and some() are ones I don't see used often, but are handy (some is sort of like findIndex but returns boolean).
I think it's more of an issue on developer performance - ie the word 'map' has a meaning that the developer understands, and if they read 'map', the notice that the results of the map are thrown away, it leads to a brain fart (aka 'wtf?').
forEach
has no such smell.This kind of micro-optimization is the devil's dust. Premature optimizations like this often lead to ugly code. Tools not rules!
map
with side effects… yes, that's bad practice. But one could usemap
here. It might not be necessary in the trivial case, but if we want to throw afilter
in there as a sanity check, this makes for readable code:Why not just the following?
Is that not equivalent, clear, and only iterates the list once instead of ~3 times?
What are the pros and cons of chaining array methods like the above? Perhaps easier to add another method, eg
sort()
, or some otherfilter()
...but in this instance, not so much, imo.I'm guilty of doing this. Makes total sense, since the arrays I'm iterating are mostly objects and are huge. Thanks for the article.
Nice Post
There is also a performance cost to consider when looping:
leanylabs.com/blog/js-forEach-map-...
I totally agree. when I need return something I will use map(), otherwise I use forEach()
Dude, it is not redundant map, it is immutable one. There are reasons why people are using it.
I totally agree to the point of the post. One thing I wonder is if map is used just like the bad practice example, when does garbage collector collects that isolated, unused array? will it exists for long?
Stop telling me what to do! I do what I want!
😘
Some comments may only be visible to logged-in visitors. Sign in to view all comments.