28

I was reading airbnb javascript guide. There is a particular statement, that says:

Don’t use iterators. Prefer JavaScript’s higher-order functions instead of loops like for-in or for-of.

The reason they give for the above statement is:

This enforces our immutable rule. Dealing with pure functions that return values is easier to reason about than side effects.

I could not differentiate between the two coding practices given:

    const numbers = [1, 2, 3, 4, 5];

    // bad
    let sum = 0;
    for (let num of numbers) {
     sum += num;
    }
    sum === 15;

    // good
    let sum = 0;
    numbers.forEach((num) => {
     sum += num;
    });

    sum === 15;

Could anyone explain, why should forEach be preferred over regular for loop? How does it really make a difference? Are there any side effects of using the regular iterators?

Jatt
  • 665
  • 2
  • 8
  • 20

6 Answers6

24

This reasoning in Airbnb style guide applies to array methods that are used for immutability, which are filter, map, reduce, etc. but not forEach:

This enforces our immutable rule. Dealing with pure functions that return values is easier to reason about than side effects.

So the comparison is more like:

// bad
let sum = 0;
for (let num of numbers) {
 sum += num;
}
sum === 15;

// bad
let sum = 0;
numbers.forEach((num) => {
 sum += num;
});

sum === 15;

// good
const sum = numbers.reduce((num, sum) => sum += num, 0);

sum === 15;

Generally, for > forEach > for..of > for..in in terms of performance. This relationship is uniform in almost all engines but may vary for different array lengths.

forEach is the one that was significantly improved in latest Chrome/V8 (almost twice, based on this synthetic test):

Since all of them are fast, choosing less appropriate loop method just because of performance reasons can be considered preliminary optimization, unless proven otherwise.

The main benefits of forEach in comparison with for..of is that the former be polyfilled even in ES3 and provides both value and index, while the latter is more readable but should be transpiled in ES5 and lower.

forEach has known pitfalls that make it unsuitable in some situations that can be properly handled with for or for..of:

  • callback function creates new context (can be addressed with arrow function)

  • doesn't support iterators

  • doesn't support generator yield and async..await

  • doesn't provide a proper way to terminate a loop early with break

Estus Flask
  • 206,104
  • 70
  • 425
  • 565
  • 3
    I did not understand the opening line `This reasoning in Airbnb style guide applies to array methods that are used for immutability, which are filter, map, reduce, etc. but not forEach:` Why `forEach` cannot be used for immutability? – Jatt Mar 23 '18 at 04:34
  • 2
    It can. But it isn't immutable by nature. New value isn't provided for you automatically (like with `reduce` or `map`). You mutate a variable inside loop that was defined elsewhere (`sum` in your example). You could modify existing object the same way and thus break 'immutabile rule' (well, you could do that with `reduce` too, but it's commonly considered a mistake). `for` and `forEach` are equally 'bad', according to this reasoning, the fact that `forEach` has a function doesn't affect anything in this regard, because we can't return the result from this function. – Estus Flask Mar 23 '18 at 11:34
  • 1
    I consider this reasoning valid (although often prefer `for..of` for readability), but not the conclusions they made. The chapter https://github.com/airbnb/javascript#iterators-and-generators is contradictory and inconsistent. In one place it states that side effects should be avoided, and then they do side effects inside forEach and label it as 'good'. Remember, it's just style guide. Style guides are opinionated and don't always make sense. – Estus Flask Mar 23 '18 at 11:35
  • 1
    Unless there's a fundamental reason why some constructs are much harder to optimize for, these kinds of benchmark comparisons are brittle. For example, on the most recent v8 version 10.4.123.20 I'm finding that `for...of`, `for`, and `forEach` basically have the same performance. – Meow Aug 09 '22 at 17:39
16

That makes no sense. forEach should never be preferred. Yes, using map and reduce and filter is much cleaner than a loop that uses side effects to manipulate something.

But when you need to use side effects for some reason, then for … in and for … of are the idiomatic loop constructs. They are easier to read and just as fast, and emphasise that you have a loop body with side effects in contrast to forEach which looks functional with its callback but isn't. Other advantages over forEach include that you can use const for the iterated element, and can use arbitrary control structures (return, break, continue, yield, await) in the loop body.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • 6
    n.b. Eric Lippert has essentially said the same about `foreach` vs. `.ForEach(...)` in C#: https://blogs.msdn.microsoft.com/ericlippert/2009/05/18/foreach-vs-foreach/ Seems like the airbnb people are falling into the pitfall of thinking something that looks functional is preferable just because it looks functional. – JLRishe Mar 22 '18 at 05:20
3

The below is a list of advantages I see between the two.

Advantages of "for of":

  1. In dev-tools, you can see the value of all variables in the function just by hovering. (with forEach, you need to change which stack-entry you're in to see variables not accessed within the loop function)
  2. Works on all iterables (not just arrays).
  3. You can use break or continue directly, instead of needing to use the some function and doing return true, which reduces readability. (can be emulated)
  4. You can use await within the loop, with the async/await context preserved. (can be emulated)
  5. Code within the loops can directly return out of the overall function. (can be emulated)

Advantages of "forEach"

  1. It's a function call like map and filter, so it's easier to convert between the two.
  2. It doesn't need transpilation to be used in pre-es2015 contexts (just needs a simple polyfill). This also means you don't have to deal with odd "error catching" from the transpilation loop, which makes debugging in dev-tools harder. (depends on transpilation tool/options)
  3. You can make a "custom forEach" method with added features. (here is a version that adds support for break, continue, return, and even async/await)
  4. It's a bit shorter. (for-of is 5 characters more if using const -- 3 if using let/var)
Venryx
  • 15,624
  • 10
  • 70
  • 96
3

forEach iterations cannot be delayed by await. This means you cannot use forEach to pipeline, let's say, web requests to a server.

Also forEach is not present on async Iterables.

Consider the following cases:

1

let delayed = (value)=>new Promise(resolve => setTimeout(() => resolve(value), 2000));


(async ()=>{
    for(el of ['d','e','f'])console.log(await delayed(el))
    
})();


(async()=>{
    
    ['a','b','c'].forEach(async (el)=>console.log(await delayed(el)))
   
})();

Result:

d
a
b
c
e
f

The elements of [d,e,f] array are printed every two seconds.

The elements of [a,b,c] array are printed all together after two seconds.

2

let delayed = (value)=>new Promise(resolve => setTimeout(() => resolve(value), 2000));

async function* toDelayedIterable(array) {
    for(a of array)yield (await delayed(a))    
}


for await(el of toDelayedIterable(['d','e','f']))console.log(el)

toDelayedIterable(['a', 'b', 'c']).forEach(async(el)=>console.log(await el));

Result:

d
e
f
Uncaught TypeError: toDelayedIterable(...).forEach is not a function

The elements of [d,e,f] array are printed every two seconds.

Error when trying to access forEach on the asyncIterator of [a,b,c] array.

Community
  • 1
  • 1
Marinos An
  • 9,481
  • 6
  • 63
  • 96
2

Majority of the time, the Airbnb styleguide tries to keep things consistent. This doesn't mean that there are never reasons to use a for loop or a for-in loop, like suppose you want to break out of the loop out early.

The immutability comes into play when using a for loop to the change the value of something. Like reducing an array to an integer or mapping over elements to generate a new array. Using a built-in map, reduce, or filter, will not directly mutate the array's value directly so it's preferred. Using forEach over a for/for-in loop enforces the style consistency of using a higher order function over an iterator, so I believe that's why it's being recommended.

  • Could you explain the statement `This isn't the same for forEach` Why would `forEach` mutate the array's value as compared to other methods like `map`, `reduce` , etc.? – Jatt Mar 23 '18 at 04:39
  • `forEach` doesn't inherently mutate the array's value. I meant that unlike `map` or `filter`, `forEach` will always return `undefined`, it doesn't have a return value and can be used to mutate the array (should have clarified this better). Updated. – Sharmila Jesupaul Mar 23 '18 at 15:23
-2

see the Performance of foreach and forloop here

only for readablity we are using foreach. Other than that for loop is prefered for browser compatibality ,performance and native feel.

John Willson
  • 444
  • 1
  • 3
  • 13
  • The link that you gave suggests, `forEach` is much faster. – Jatt Mar 22 '18 at 05:11
  • *"There is no difference"* - He's talking about the V8 engine. That's the difference. On safari `for` is consistently faster than `foreach` – radarbob Mar 22 '18 at 06:16
  • @Bergi There is a difference. AIK, for > forEach > for..of > for..in in terms of performance, this is uniform in almost all engines. There's still no parity, you can check it yourself https://jsperf.com/for-vs-foreach-vs-for-of . Interestingly, forEach is the one that was significantly improved in latest Chrome version. – Estus Flask Mar 22 '18 at 08:44