Compare for Loop and forEach Function in JavaScript

circle

Introduction

I found an interesting discussion in a pull request for rxjs project.

The PR author would like to refactor this part of code

1
2
3
4
5
6
7
8
9
if (_infiniteTimeWindow) {
for (let i = 0; i < len && !subscriber.closed; i++) {
subscriber.next(<T>_events[i]);
}
} else {
for (let i = 0; i < len && !subscriber.closed; i++) {
subscriber.next((<ReplayEvent<T>>_events[i]).value);
}
}

into the following format

1
2
3
4
5
6
7
8
9
10
11
12
13
if (this._infiniteTimeWindow) {
_events.forEach(event => {
if (!subscriber.closed) {
subscriber.next(<T>event);
}
});
} else {
_events.forEach(event => {
if (!subscriber.closed) {
subscriber.next((<ReplayEvent<T>>event).value);
}
});
}

So basically, the author would like to refactor the for loop by using forEach() in array utility functions.

As we know, JavaScript / TypeScript has a set of powerful array utility functions including forEach(), map(), reduce() and so on. Those utility functions greatly improve our productivity in array operations by using functional programming paradigm and they are clearer to read and easier to understand.

I believe the above refactored codes also intend to achieve a better readability and maintainability, but the member of rxjs library rejects the changes and claim that it is

removing optimizations that we had done on purpose

Is that really true? To prove that using a for loop is more efficient than forEach(), I did the following experiment.

Experiment: Comparison with Simple Value Addition and Assignment

The following codes are for comparison between for loop and forEach() using an array of 20 consisting of millions items simple value addition and assignment where the execution times are printed.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const list = Array(20000000).fill(0);
const list1 = [];
const list2 = [];

// 20 millions records with a for loop
console.time("for loop");
for (let i = 0; i < list.length; i++) {
list1[i] = i + 1;
}
console.timeEnd("for loop");

// 20 millions records with forEach()
console.time("forEach()");
list.forEach((_, i) => {
list2[i] = i + 1;
});
console.timeEnd("forEach()");

// Sanity check if the list1 and list2 are with the same calculation results
console.log(
list1.every((item, index) => item === list2[index]) &&
list1.length === list2.length
);

I run the codes for 5 times and get the following results.

for forEach()
1 408.801ms 629.787ms
2 410.799ms 629.338ms
3 409.691ms 631.505ms
4 390.361ms 612.205ms
5 390.503ms 618.609ms

So the result is very clear that for loop performs better than forEach(). Although there are 20 million records, the operations are simple, only value addition and assignment. What if the operations are more complexed such as demonstrated in rxjs, every subscriber is notified and certain following actions are triggered? It will become a big difference. So, when you are working on a library, it’s critical to think about the performance, even if for loop looks “more ugly” than forEach()

Besides the performance, what are the other differences?

Break Loops

If you want to exit the loop based on certain conditions, can forEach() do that? The answer is yes, but it’s more troublesome and less straight forward compared to for.

In for loop, it’s easy to break the loop by

1
2
3
4
5
6
7
8
9
10
const list = [1, 2, 3, 4, 5];

for (let i = 0; i < list.length; i++) {
if (list[i] > 3) {
break;
}
console.log(list[i]);
}

// Only 1, 2, 3 are printed.

However, in forEach(), you cannot directly use break as SyntaxError: Illegal break statement is thrown. It’s because break keyword can only be used in a for loop, while a forEach() accepts a function as a parameter and break keyword is NOT allowed in a function.

1
2
3
4
5
6
7
8
9
const list = [1, 2, 3, 4, 5];

list.forEach(item => {
if(item > 3) {
break;
}
console.log(item);
})
// SyntaxError: Illegal break statement

You can still use a hack way to achieve the break of loop, by using a try-catch block (inspired by HERE).

1
2
3
4
5
6
7
8
9
10
11
12
const list = [1, 2, 3, 4, 5];

try {
list.forEach(item => {
if (item > 3) {
throw Error();
}
console.log(item);
});
} catch (e) {}

// Only 1, 2, 3 are printed

Or another hack way is using some(), because once the condition in the callback function is fulfilled, it will stop checking the rest of the items

1
2
3
4
5
6
7
const list = [1, 2, 3, 4, 5];

list.some(item => {
console.log(item);
return item > 2; // 3 is changed to 2 because the condition checking happens after the console.log()
});
// Only 1, 2, 3 are printed

However, neither of the ways looks pretty as they are not straight forward nor intuitive.

  1. try-catch block is meant for catching errors, but there is actually no error at all.
  2. some() is conceptually not meant for breaking a loop, but it just happens to have the same functionality of break.

Therefore, it takes extra time for developers to figure out the outcome of these pieces of codes, which is also likely to cause careless mistakes and confusions.

Conclusion

In this article, we have discussed the differences of for loop and forEach().

To summarize, I would recommend to use for loop when

  1. You have to break in the loop
  2. You have a strict requriement on performance

If you don’t have the above 2 conditions, it’s actually better to use forEach() because it looks clear and concise and is better to comprehend.

Featured image is credited to Thor Alvis on Unsplash