Sometimes using loop for array iteration can be bad. Let's see how?
Let's create a 10
empty slot array using Array constructor.
const array = Array(10);
console.log(array); //[empty × 10]
Now let's iterate over the array element using for loop. This loop will log Hi
10 times to the console, which we do not want to do.
But our for loop iterates over the empty slot, that is bad for our software performance.
const array = Array(10);
const len = array.length;
console.log(array); //[empty × 10]
for(let i = 0; i < len; i++){
console.log("Hi");
}
Let's see another example.
const array = [1, 2, 3];
array[4] = 10;
const len = array.length;
for(let i = 0; i < len; i ++){
console.log("Hi");
}
In the above snippet index 3
of the array is an empty slot, but the console.log
will execute 5 times.
Now let's try using one of the array methods.
forEach()
method invoked the passed callback function for each element.
But in this case our array slot is empty and our forEach()
method is not going to invoke the log
function for the empty slot.
So, the below code will print nothing to the console.
const array = Array(10);
const log = () => console.log("Hi");
array.forEach(log); // NOT GOING TO INVOKE THE LOG FUNCTION
If we try our second for...loop
example using forEach()
method it will execute 4 times. The passed callback function not going to invoke for empty slot.
const array = [1, 2, 3];
array[4] = 10;
const len = array.length;
array.forEach(() => {
console.log("Hi");
})
Also you can try .map()
method insted of for..loop
.
Top comments (38)
I'm actually quite surprised that
forEach
skips the empty slot. That is odd behaviour if you ask me, and certainly not what I would expect. Also, as far as I am aware, using the traditional for loop is also considerably faster thanforEach
- so I'm not sure how it would be bad for performance?Surly Jon,
for...loop
is on of the fastest looping mechanism. In the example we are usingforEach()
for the exceptional behavior. Under the hoodforEach()
using something like the below code.Using
forEach()
can skip some unnecessary heavy calculation. What if we accidentally setarray[400] = "SOME VALUE"
and the other slots are empty, then we have to unnecessarily execute our heavy calculation function400
times that will affect the performance.If you're accidentally setting
array[400]
then that is your problem, and it needs fixing. The heavy calculation is merely a side effect of your bugyes, that is the point of my post how we can write good quality code, where can we do the mitake? if we do mistake in our code it can be affect the performance and create unnecessary bug.
It's still a micro optimization. The for loop can loop through 400 empty places in an array in what, 5ms? It's extremely fast.
Precisely - and by addressing the symptom rather than the cause i.e. by switching from a 'for loop' to
forEach
- you would actually be making the performance of the optimal (bug fixed) system worse than before.This is a code smell it's not much to do with for loops being bad they are doing exactly what you would expect.
It's better to sanitise your inputs such as
Or better, don't make 10 empty slots ☺️
You are right Adam,
no one going to create an empty slot for an array, it can happen accidentally/unintentionally and it can be bad for our application performance that is the whole point of this post.
i have updated my post using a relevant code example. Please take a look.
Thanks for your feedback.
Graciously I admit I did learn more foibles about the forEach method in this post, not iterating over empties is not what I expected at all, thanks for bringing it up!
It doesn't matter what function you pass to
filter()
- it skips holes.ECMAScript 6: holes in Arrays.
I imagine you misunderstood my code
I imagine that you were trying to use
to sanitize the array. However observe
i.e. holes are skipped, actual
undefined
values are not.So the function passed to
filter()
never gets to make a decision about the holes.sparse arrays vs. dense arrays
The intent is to remove All falsey values which I admit is a bit of a hammer and yes I can see your point, filter ignores empties, but it still cleans an array which is my point.
To anyone not aware, This is a well known trick which removes ALL falsey value.
If you want to handle falsey valuess such as 0, -n, false, null, undefined, write a better predicate function, my demo is just that, a quick demo of principal, prepare your inputs.
I use loop or array methods depending on what/which purpose i'm using it for.
In the below code example
for loop
orforEach()
will not going to serve my purpose in a easy to readable/understandable way.for of works with async not forEach
Higher order functions makes code cleaner and easy to understand. forEach may be simple but filter, map or reduce makes tedious operations very easy to implement.
Yes,
forEach()
method also a higher order function.But as i mention before that, i use those methods depending on the context where it fits the most.
Tip !
You can use for...of to loop over an array and for...in to loop over keys of an object.
The idiomatic way to detect holes in arrays is with the in operator;
If you need to be paranoid about third party libraries modifying prototype objects:
For a more detailed discussion of holes see ECMAScript 6: holes in Arrays.
While
map()
doesn't call the iteratee on holes it does preserve the holes in the array it returns. The point being - the use case formap()
(producing a new array of identical length containing transformed values) is much narrower than that of afor…loop
. The behaviour ofmap()
is consistent with treating holes asNone
while values are treated asSome(value)
. This is problematic when one needs to replace the holes with some default value - in that case it is necessary to use Array.from():Ultimately the entire
for…loop
considered harmful attitude is misguided. Iteration is often required without arrays being involved. So it is valuable to be able to write a readablefor…loop
.Higher order functions (HOFs) like
reduce()
,map()
andfilter()
have their uses butforEach()
is the oddball as it needs a function with side effects to be useful.Iteration on iterables doesn't have direct access to the Array HOFs, so either they need to be converted to intermediate arrays (e.g. with
Array.from()
) or be processed with for…of.forEach()
exists on Maps, Sets, DOMtokenList, NodeList, TypedArray, etc.But I fail to see the advantage of
over
or perhaps
Perhaps the real solution is to clean up the loop bodies of
for…loop
so that they are easier to read.In functional languages recursion is used as the fundamental means of any iteration:
This works in JavaScript but JavaScript isn't a functional language - and none of today's JavaScript engines support tail call elimination, so recursion is usually avoided to avoid blowing the call stack (and sometimes to avoid the overhead of the function calls).
The fundamental means of iteration in JavaScript is the
for…loop
which means that mutation of the looping state is essential but the rest of the values can largely be immutable:i.e. for a cleaner loop body:
The idea is to organize the top of the loop body so that it could be easily refactored to:
but to then leave the loop body "as is" because it already is sufficiently cleaned up.
Basically think of arrays as key-value stores that use positive integers as keys.
The problem is that JavaScript engines optimize arrays for performance
v8.dev: Elements kinds in V8:
PACKED_SMI_ELEMENTS
->HOLEY_SMI_ELEMENTS
PACKED_DOUBLE_ELEMENTS
->HOLEY_DOUBLE_ELEMENTS
PACKED_ELEMENTS
->HOLEY_ELEMENTS
.PACKED
(dense) arrays can transition toHOLEY
(sparse) arrays - but not the other way aroundPACKED
processing is more optimized, though:That said (as already suggested elsewhere), if holes don't have any particular meaning within the processing context, it's better to sanitize
HOLEY
arrays intoPACKED
arrays rather than relying on the "skipping behaviour" of select methods.This seems to be a typo. In the example code, you initialize indexes 0, 1, and 2, then you explicitly set index 4 equal to 10, so it is index 3 that would be empty.
Thanks Jay, fixed the typo.
great!
I think for 95% of proyects readability is more important than performance.
Happily in this case they agree
But if you need to choose between the two, most always you should choose readability
Can you tell us why it is bad for software performance and how much difference does it make? Do you know how the Array.forEach method skips these empty slots?
Hi Hasnain, under the hood
forEach()
using something like the below code.Using
forEach()
can skip some unnecessary heavy calculation. What if we accidentally setarray[400] = "SOME VALUE"
and the other slots are empty, then we have to unnecessarily execute our heavy calculation function400
times that will affect the performance.That is why we almost never use index in terms of manual assignment and use push method or array spreading instead. This is why it is also recommended to never use these constructor methods, unless of course, it is an absolute requirement which is almost quite rare.
This also does not improve performance either. Both use for loops and there is no evidence that forEach is faster than the for loop.
No one says that
forEach()
is faster thanfor...loop
. For the same piece of codefor..loop
will be always faster thenforEach()
method unless we make any mistake in our code.The above example code snippets for
for..loop
are written in such a way that, we can understand what can happen if we make this kind of mistake in our code.Hmm. Thanks for writing the post.
Thanks for giving me the feedback.
For some who are not sure what forEach/map does, it should bedoing something like this:
under the hood is a while loop.
In most cases its absolutely ok to use for loop. Writing in the right scope is "easier" with for loops. But its important to know this behavior.
And the benefit of for loops is not much. So writing with tools that already exist is much better. (map, foreach, every ...)
What happens if you want to break out of the loop? You can’t with forEach. Also I would imagine forEach uses for loop under the hood.
One workaround it to abuse some() instead:
Personally I would find a
for…loop
withbreak
clearerfilter and reduce would do that and would be clearer.
Also
Array.some
gives you a boolean.I took the original comment to have the objectives of:
forEach
andfilter
will do)reduce
relies on)There is nothing wrong with the
filter
/reduce
approach, especially if the array is reasonably short (whatever that means in the context of the executing hardware) - and avoiding the intermediate array does require a bit more codeBut the focus on array HOFs seems to obscure to some people that functions can be composed in a variety of ways.
I also find that in many cases array HOFs make code less readable because the authors insist on using inline, multi-line anonymous functions (forcing a full mental parse of the code) instead of spending a few more lines of code to separate them out and assign them a "Descriptive And Meaningful Name" (related to DAMP) that can be used with the HOF.
In my example you could easily write functions for filter and reduce in a declarative way. Which is understandable.
Readability:
filter
andreduce
are much better readable as the gatherUntil function.Performance (because they all talk about performance in JS)
when was the last time you had performance issue and tought: 'maybe its because of THIS type of loop' in JS?
normally when you write JS you have more performance issues somewhere else as iterating through array's.
Yes, for loops are faster. But you write more code and more code means more bugs. As you can see my little example has just some lines. There is not much space to have a bug here.
Only because something is faster makes it not better for programming, then you wouldn't write JS in first place.
I prefer a more "functional" style of doing things. And would recursion not be so slow (and the stack gets full) in JS i would do that.
and im not quit sure if for loops are slower as reversed while loops.
Because in this particular case
gatherUntil
is just as utilitarian asfilter
andreduce
- which still focuses more on the "how" rather than the "why" - declarative or not. Ideally the functions "descriptive and meaningful names" should be more DSL-like, focusing more on the "why" than the "how".Whenever I start to play around with performance comparisons (example based on this) the good 'ol
for…loop
seems to perform consistently well across multiple JavaScript engines - while the "other" ways tend to be all over the place.No Code, no bugs. Other than that studies typically focus on total application length when they state "larger code bases contain more bugs" - but complex applications would be expected to be a.) larger and b.) be more prone to bugs. That doesn't necessarily imply down at the lower level that 1 line is always better than 5 unless this is systemic across the entire code base leading to a significantly larger application. Code Golf emphasizes brevity but tends to lead to code that isn't easy to understand. If there is something to "Code inspection is up to 20 times more efficient than testing" then making code understandable to the broadest possible audience of reviewers is important (within reason - because … trade-offs). Even in A Large-Scale Study of Programming Languages and Code Quality in Github:
i.e. as always … it's complicated.
Another argument against "more lines" is that more lines take more time to parse. But not all lines of code are created equal - what matters is the actual workload on the interpreter/compiler during runtime after the parse.
You did notice that people here expressed surprise about
forEach
,filter
, andreduce
skipping holes? Given that processing tends to need to skipundefined
values there often isn't a problem - untilundefined
values and/or the difference between anundefined
value and a hole becomes significant. So even on a single line there is space for bugs if code authors/reviewers have an incorrect conceptual model.Back in 2009 the video game industry started to abandon OO programming for Data-Oriented Design because OOP imposed too much overhead on commodity hardware. Back then this was largely applied to games written in C/C++ but the trend has lead Unity to adopt DOTS (Data-Oriented Tech Stack) for .NET/C#. Data-Oriented Design aims to optimize for the platform rather than developer experience (ECS - again it's a trade-off).
Similarly I'm imagining JS as running on the shittiest phone out there (on the back end you have other options right?) - the whole Responsible JavaScript thing.
The web is still the most hostile development platform in the world even more so for hyper-constrained devices (Techniques to make a web app load fast,, Setting up a static render in 30 minutes). That level of effort is a far cry from "press of a button, comfy chair development".
Sure, but at its core JavaScript is imperative and not as effective at appropriating functional practices as Rust. Most fundamentally it doesn't have "immutability by default" through core-supported, optimized persistent data structures - so mutability is par of the course and it falls to the author to use it responsibly and effectively. I view JavaScript as function-oriented.
"Slowness" of recursion in JS would be due the "overhead" of the repeated function invocations. You don't seem to be too concerned about the repeated invocations of the functions that are passed to
reduce
,map
,filter
etc. (something that could be addressed by putting the logic into a loop body; on the other hand inlining can be tricky). Bounded recursion is doable but simply not optimized for as thefor…loop
is the most basic (and performant) mode of iteration in JS.