It is current year new Date().getFullYear()
, and still I still see many JavaScript devs using and even recommending the use of for
loops. You yourself may even be using for
loops on a regularly basis. Here's why you shouldn't and what you should use instead.
Why shouldn't you use for
loops?
for
loops and their usage are largely a holdover from imperative languages, in particular C-likes and their derivatives. While they are quite versatile, they suffer many flaws.
Let's start with clarity. Let's face it, for
loops just don't make the prettiest blocks of code, and only increase in ugliness the larger they get. In complex algorithms, they can quickly become enigmatic, nigh unreadable save for commenting. And while comments are an expected courtesy, there's a reason why bad documentation is such a frequent joke amongst devs.
Secondly, for
loops increase the chance of unwanted side effects. for
loops--and while
loops-- are what's known as control statements, meaning they must be placed inside of a function and can not be used as standalones. This inherently increases the chance you'll end up manipulating variables outside of the loop's scope. Unfortunately, this clashes with more modern programming paradigms in functional programming, specifically precluding anything involving for
loops from being a pure function.
Thirdly, for the exact same reason as above, for
loops are prone to requiring mutation in some form. A lot of the time, this is also unavoidable because all for
loops can really do is facilitate iteration. By themselves, for
loops are kinda useless, which means you have to declare a variable to mutate, or mutate the original array. Again, this is at odds with functional programming principles.
What should you use instead?
As of ES6, Array.prototype
methods have been introduced that make for
loops obsolete in most ways.
Let's recap the reasons against for
loops and how these methods solve them.
1. Lack of clarity.
In ideal circumstances, good code should be self-evident and self-explanatory. Array methods bring us closer to that ideal, they are almost descriptive enough to read as some sort of "shorthand natural language".
myArray.every(value => value > 40);
// Check every element of myArray is greater than 40.
myArray.filter(word => word.length > 6);
// Filter out every element in myArray longer than 6 characters.
myArray.sort();
// Sort myArray (lol)
Contrast these with their for
loop equivalents:
const checkEveryValueGreaterThanForty = myArray => {
for (let i = 0; i < myArray.length; i++) {
if (myArray[i] <= 40) return false;
}
return true;
};
const checkEveryWordLongerThanSix = myArray => {
for (let i = 0; i < myArray.length; i++) {
if (myArray[i].length <= 6) return false;
}
return true;
};
// Most devs already know to use Array.prototype.sort(), consider this
// a more extreme example of what enigmatic for loop code can look like
function bubbleSort(myArray) {
let isSorted = false;
while (!isSorted) {
isSorted = true;
for (let i = 1; i < myArray.length; i++) {
if (myArray[i - 1] > myArray[i]) {
isSorted = false;
[myArray[i - 1], myArray[i]] = [myArray[i], myArray[i - 1]];
}
}
}
return myArray;
}
Yikes. As you can see, these are ugly, long, and you actually have to spend time reading through the entire thing to figure out what's going on. Using array methods cut them down into a single beautiful line with an immediately recognisable purpose.
2. Side effects.
Many array methods are higher-order-functions -- they take in a callback function as one of their parameters. By explicitly calling array instance methods, iteration can be performed in something like a self-enclosed scope. You may have already noticed this in the previous examples, where loops inside functions were replaced by one-line methods.
3. Possible mutation.
Accessor methods are handy for modifying an array, and iteration methods replace most if not all functionality provided by for
loops without touching the original array.
const doubleArrayValues = myArray => {
const newArray = [];
for (let i = 0; i < myArray.length; i++) {
newArray[i] = myArray[i] * 2;
}
return newArray;
};
Notice in this example we've had to create a new variable newArray
. This is necessary if we do not wish to change myArray
, but even still, we must mutate newArray
in order for the algorithm to work. What if we tried an array method?
myArray.map(value => value * 2);
We have eliminated the need for a new variable without compromising our desire to keep the original array unchanged.
In addition, there are mutator methods such as Array.prototype.pop()
, Array.prototype.sort()
, and Array.prototype.splice()
. What this provides, especially when chaining methods, is greater flexibility in how you approach an algorithm.
That's not to say you can't mutate using non-mutator methods. However, you'd have to go out of your way to mutate the original array:
myArray.forEach((element, index) => {
myArray[index] = element * 2;
});
Kinda defeats the purpose, but once again, you have the flexibility to do so if you really want to for some reason (you shouldn't).
The bottom line.
Array methods, when applied properly, are incredibly powerful and render for
loops all but obsolete. It is possible to write very pure functions, without mutating any arrays or objects and without creating any variables or constants. For example, the popular Combination Sum interview question can be purely solved using only recursion and array methods.
// Array.prototype.reduce() is incredibly powerful when applied correctly
const combinationSum = (sum, numbers) =>
numbers
.filter(filterNumber => filterNumber <= sum)
.reduce((path, currNumber) => {
if (currNumber === sum) return [...path, [currNumber]];
else
return combinationSum(sum - currNumber, numbers).reduce(
(allCombos, currCombo) => [...allCombos, [currNumber, ...currCombo]],
path
);
}, []);
Of course, for
loops are still around for a reason. Perhaps you have a specific application where it's not appropriate to, or just can't use array methods. In those cases, take care to give your counter a proper name if you intend to use it -- something like let index = 0
instead of just let i = 0
. In any other case, try your hand with array methods.
Top comments (37)
... And it's articles like this that make me think we were better off before anyone could publish anything on the internet.
The main problem I have is your piece is actually well written, thus it is probably going to be taken as gospel by newbie devs.
I enjoy the ease of writing callback functions instead of for loops as much as anyone, but it is such a biased outlook to claim that this is always the right solution.
Why don't you mention the areas where imperative loops outshine higher order functions? E.g. the fact that a for loop is ~10x faster than a
forEach
callbackI believe this is untrue. Can you substantiate this claim?
It's widely known that loops based on callback functions generally perform much worse than imperative loops. Here's an example, where
forEach
is 96% slower than a vanillafor
loop: stackoverflow.com/questions/438217...C'mon man, I appreciate that you want to share your knowledge, but if you are going to write posts with such provocative titles then you need to know this stuff. I am a big fan of functional programming and much prefer it over OOP, but the aspect I dislike most about FP is the performance hit from making everything immutable
Try yourself.
let arr = [];
for(x = 0; x < 100000; x++) {arr.push(Math.random());}
function oldFor(d) {
aux = [];
const t0 = performance.now();
for(x=0; x < d.length; x++) {
aux.push(d);
}
return (performance.now() - t0);
}
function eachFor(d) {
let aux = [];
const t0 = performance.now();
d.forEach(v => {
aux.push(v);
});
return (performance.now() - t0);
}
oldFor(arr);
eachFor(arr);
Okay. This is your code in typescript with a 10M length array. The forEach time is double.
I made another for just testing the other loops with a 10M length array.
So... the conclusion is... If you want to loop through big arrays just use the old for loop. And I made an account just for to send this comment.
wow, this is terrible. I'd suggest "deleting this post" if you were to ask.
The title is so clickbait, it can confuse people a lot.
Your examples are not about "stop using" but about situations how/when to use
filter
,reduce
, etc.I think you should provide more specific examples and real life use cases to explain your opinion, otherwise it's a really bad advice.
I would like to filter all those kind of articles.
To be clear I appreciate your effort, but radical attitudes saying that you should not use x or y is harmful. Lately there was such attitude for not using
reduce
, and I don't agree with that in the same way I don't agree with your article.Also I think you are by intention making such clickbait title.
Good one 😁
If you are working with very large lists of data, the for loop shines like a diamond
Or if you read a very big file, or a result set of unknown size.
+1 about
reduce
. Consider #3 in dev.to/thejohnstew/5-programming-p...It uses
reduce
to feel functional and hip, but in practice it just mutates the arrays and passing them as is (they are mutated, but it's the same reference) to the next iteration.But even proper uses of
fold
/reduce
are usually clearer and simpler when done with loops, because the whole point ofreduce
was to emulateforeach
in purely functional languages.All of these are tools that serve different purposes. A few points I'd like to make:
1.) Composition
The prototype methods
map
,filter
,reduce
are useful because they avoid side effects and mutation by returning a new instance of the array, but they're also composable (or chainable in this chase), arguably their biggest benefit.The other methods,
sort
,every
, andforEach
are either not composable because they don't turn arrays, or they actually does mutate in place (which you do touch on in the article with forEach, much appreciated, but sort is the bigger offender).2.) OG for loops are generally faster
hackernoon.com/javascript-performa...
Not only that, but they can take exponentially less resources. Imagine you have a data set of N objects, that all get mapped, filtered, and reduced via prototype methods. That's three new versions of your data of N length that have to be in memory now, and you've gone through the list 3 times. There're real performance implications to this. If you can do it with a for loop in one pass, that may be a better option in context.
3.) For...of
The new for...of operator is outstanding, and plays better with async/await than the array prototype methods. Take a composition function for example:
This is basically a reduce, but it is easier to understand and write with a for...of loop. Try an implement this with the reduce prototype method, and I think you will see that it's not as straightforward.
Not only that, but For...of's support iterables, one of JavaScript's most powerful features. To use Array prototype methods, you have to spread them across a new array and you lose the context about what the data structure was initially. You then lose all the benefits of them- whether it's a Set or Map or some custom iterable. To me, that's pigeonholing yourself to array features and JS will evolve past that.
Always appreciate a different opinion though. Thanks for writing this and starting the conversation!
I actually enjoyed your piece. I think you're getting piled on.
There are good reasons to use
.forEach()
,for
andwhile
. Just remember, withforEach()
it's just an abstraction - it's basically sugar on top of afor
loop. This makes it quite a bit slower, but I'd agree syntax is often much cleaner.Maybe unintentionally, you're veering into a larger discussion on performance and picking the right data structure to use in a given situation. You might want to follow this piece up with a part 2 where you analyze those further.
Agreed on most counts. You don't mention that
for
loops also allow you to break out of the loop early; which depending on context can provide performance benefits.On the contrary, there are some incredibly useful applications for
reduce
and it can often be used when a normalfor
loop seems like the only option.You can't use BREAK or CONTINUE in a foreach/filter/map etc. Normal for loops are there for a reason.
My point is that
for
loops shouldn't be the first solution, but rather used on a "as needed" basis. I acknowledge in my article there are circumstances where they may be preferable.Could you justify this statement?