Paul B
6 min readFeb 6, 2019

--

Thanks for taking the time to write the article, I enjoyed it.

I think in terms of server side programs, there’s definitely a strong case for using for… of loops over Array’s higher order functions. Server’s likely to be processing larger amounts of information, where performance will become an issue. Possibly also desktop applications too.

But in a browser I think it’s unlikely to encounter noticeable performance gains unless you’re working on something like complex animations or games etc. For the average list of todos or posts or products, it’s often the difference between 5 and 10ms…

And, as I’ll eventually explain, for… of loops are likely to be less performant than array iteration in browsers. (This has kind of turned into its own article)

I completely agree that the function in my final for loop was redundant, and for loops are definitely an easier way to learn about iteration. But my example assumes readers already have a grasp on both methods of iteration… as the article’s about the performance of array iteration vs iterators...

So pardon the pun, but I have reiterate:

The benefits of using array iteration over for… loops are far greater than any performance gains.

Of course, readers who are only ever going to be working on server side or desktop applications where they have complete control over the JS engine running their code, you can skip the rest… But if your code is ever likely to be used in a web browser, read on…

One really great tool I’ve found for learn to write better code is linting rules. I strongly encourage everyone to work with an opinionated ruleset like airbnb.

On the first project I worked on using the ruleset, I found it to be a thoroughly frustrating experience. A lot of warnings I was getting felt counterintuitive and advice seemed to be the opposite of what I’d understood to be the “right” way to compose things.

One great example of this is the for... of linter warning:

iterators/generators require regenerator-runtime, which is too heavyweight for this guide to allow them. Separately, loops should be avoided in favor of array iterations. (no-restricted-syntax)

Loop avoidance is very simple to understand: loops are much harder to reason about than array iteration.

From airbnb’s rules:

Why? This enforces our immutable rule. Dealing with pure functions that return values is easier to reason about than side effects.
https://github.com/airbnb/javascript#iterators

In other words, there’s a greater risk of introducing bugs to our program when we use for... of loops over array iteration.

It’s easy for less experienced developers to make a mistake like this:

for (const i of arr) {
if (arr[i] % 2) {
arr[i]+=1;
}
}

Our intention is that [1, 2, 3] should give us [2, 2, 4], but this loop will give us [1, 2, 4]. So we might try another set, say [4, 3, 5, 7] expecting [4, 4, 6, 8] but getting [4, 3, 5, 8] instead.

Younger me encountered problems like these many times. Now it’s obvious to me that whoever wrote this loop was getting confused with another pattern:

for (let i = 0; i < arr.length; i++) {
if (arr[i] % 2) {
arr[i]+=1;
}
}

The issue is that our results are misleading. It’s just a coincidence that one value in each of our test cases was coincidentally an index of its array… so we get a few false positives, further obscuring the true nature of our problem.

The three main issues with our solution are:

  • Confusing one pattern with another.
  • Mutating our source array.
  • Using a confusing variable name, which typically is used to represent an index.

To older me, it’s obvious that the correct solution using a for.. of loop is:

const makeEven = [];
for (const val of arr) {
makeEven.push(val % 2 ? val : val + 1);
}

Now this solution has so many use cases, so we wrap it up in a function:

const makeEven = arr => {
const makeEven = [];
for (const val of arr) {
makeEven.push(val % 2 ? val : val + 1);
}
return makeEven;
};

It’s becoming pretty obvious what kind of function this is… The general solution is:

const map = (arr, fn) => {
const map = [];
for (const val of arr) {
map.push(fn(val));
}
return map;
};

Hey, we already a function for that!

const makeEven = arr.map(val => val % 2 ? val : val + 1);

This is much easier to reason about because all we have to consider is:

val % 2 ? val : val + 1;

To test it, we can just do something like this:

const fn = val => val % 2 ? val : val + 1;
for (let i = 0; i < 5; i++) {
fn(i);
}

We avoid mutation, side effects and confusion, and we save time.

It’s slightly less clear what is meant by iterators/generators require regenerator-runtime, which is too heavyweight for this guide to allow them

In essence, there’s a lot less to understanding required to use higher order functions than we do a for... of loop.

One great explanation is:

`for..of` will not be allowed by the airbnb preset because it requires Symbols to exist, and Symbols can not truly be polyfilled — and regenerator-runtime is too heavyweight. This concern outweighs the minor concern that side-effecty iterations (which are rare) are slightly more statically detectable as `for..of` than as `.forEach`.
https://github.com/airbnb/javascript/issues/1271#issuecomment-283736133

Put simply, for… of loops are vastly more complex and difficult to understand than Array iteration. They require knowledge of generators, iterators and symbols to truly comprehend what’s taking place in the loop…

In other words, in browserland, when we think we’re saying:

const makeEven = [];
for (const val of arr) {
makeEven.push(val % 2 ? val : val + 1);
}

What we’re really saying is:

var makeEven = [];
var _iteratorNormalCompletion = true;
var _didIteratorError = false;
var _iteratorError = undefined;
try {
for (var _iterator = arr[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
var val = _step.value;
makeEven.push(val % 2 ? val : val + 1);
}
} catch (err) {
_didIteratorError = true;
_iteratorError = err;
} finally {
try {
if (!_iteratorNormalCompletion && _iterator.return) {
_iterator.return();
}
} finally {
if (_didIteratorError) {
throw _iteratorError;
}
}
}

Meanwhile, when we say:

const makeEven = arr.map(val => val % 2 ? val : val + 1);

It’s more or less what we’re saying:

var makeEven = arr.map(function (val) {
return val % 2 ? val : val + 1;
});

And one of these is clearly more performant than the other: https://jsperf.com/trhq-iterator-vs-hof/

  • Pure for... of loop is ~50% faster than map
  • Transpiled for... of loop is ~70% slower than transpiledmap

In reality, many projects in the wild only serve transpiled bundles, so we’re punishing a users of most browsers for using a pattern that also punishes developers writing the code.

Even if we have the luxury of working with a build that produces modern and legacy bundles, we should be serving a solution that provides consistent performance across builds... Without writing a test, I’d wager that non-transpiled map and transpiled map perform about the same.

And also, if we’re going to consider performance gains for one or the other, we should favour our legacy build over modern… as it’s highly more likely that their legacy browser is running on an older device…

And for... of won’t even run on super old browsers without “polyfilling” Symbol. The problem with this is that it’s impossible to truly polyfill Symbol. Yes we can approximate Symbol in old Safari or IE11, but it’s adding complexity and overcomplicating things much more than using a functional approach that’s got great browser support.

So in conclusion, if this is a server / desktop project and / or we don’t care about developer experience, then ignore everything I’ve said. If this is a web project / or we care about developer experience, use array iteration.

:)

--

--