Array Method or a Loop?

Perhaps you’ve encountered discussions whether we should use Array methods (filter, map, reduce, etc.) or loops (for, for...of, etc.) to iterate over a collection. Like that tweet where Mr.doob jokes about whether web devs know about loops, or that video in which Jake and Surma discuss Array.prototype.reduce. People are generally opinionated and opinions vary wildly as seen in this Twitter thread.

Two common arguments to choose one over the other are performance and readability. Let’s take a better look at the differences between loops and Array methods and compare them, so we can decide for ourselves.

Performance

Before ECMAScript 5 all we had was loops and jQuery’s $.each. Which method of iteration was fastest was commonly debated. Things have significantly improved since then. The CPUs in our smartphones outperform the computers from back then, we now get to do more awesome stuff client-side without crashing the entire browser. Despite all that, the performance of Array methods is still brought up. Why is that?

Benchmarks don’t lie. It seems loops are consistently Array methods slower than loops in performance tests 1 2. So if you’re iterating over a large list of items, you might want to avoid Array methods.

In most cases, it shouldn’t make too much of a difference. For most projects, it’s uncommon to iterate over lists of over a hundred items. You can generally use whatever you think is more readable. Just be sure to always check performance on low-end devices.

Readability

We can agree readability is important. It is also subjective. For example, let’s take an arrow function with implicit return ((a, b) => a + b). A beginner might confuse it with a logic expression. The greater than or equal operator (>=) does look similar. Perhaps they might not realise that the function returns a value. After all, the return is implicit. A trained eye will notice arrow functions in an instant. The same can be said for all other syntaxes. It is up to us to either train beginner developers or refrain from using a feature so the codebase is accessible to developers of any skill level. That being said, let’s compare the readability of some loops and Array methods.

Array.prototype.forEach and for...of are both easy to read.

posts.forEach((post) => console.log(post));

// or

for (const post of posts) {
  console.log(post);
}

The loop equivalent of Array.prototype.filter is a bit explicit.

const drafts = posts.filter((post) => post.isDraft);

// or

const drafts = [];

for (const post of posts) {
  if (post.isDraft) {
    drafts.push(post);
  }
}

The same goes for Array.prototype.map.

const publishDates = post.map((post) => post.date);

// or

const publishDates = new Array(posts.length);

for (let i = 0; i < posts.length; i++) {
  publishDates[i] = posts[i].date;
}

Array.prototype.reduce is awesome at what it does, but might take a couple of seconds to understand what’s happening. Even for a sum-like operation, you might need a second to comprehend it. Readability quickly gets worse when the reduce callback contains more logic.

const totalWordCount = posts
  .reduce((result, post) => result + post.wordCount, 0);

// or

let totalWordCount = 0;

for (const post of posts) {
  totalWordCount += post.wordCount;
}

For other methods, like Array.prototype.every, Array.prototype.find, and Array.prototype.some, the loop isn’t terrible and they’re better in a function with an early return. They are definitely more explicit than the Array method.

const isEverythingPublished = posts.every((post) => post.isDraft);

// or

let isEverythingPublished = true;

for (const post of posts) {
  if (post.isDraft) {
    isEverythingPublished = false;
    break;
  }
}

// or in a function with early return

function getIsEverythingPublished(posts) {
  for (const post of posts) {
    if (post.isDraft) {
      return false;
    }
  }

  return true;
}

const isEverythingPublished = getIsEverythingPublished(posts);

If you prefer explicitness, the loops might appeal to you. To me, they are a bit verbose and I personally prefer the Array methods except for Array.prototype.reduce. But again, readability is subjective. It’s personal.

Features and Uses

Although it may seem that loops and Array methods are two ways to do the same thing, they are different in significant ways.

Firstly, Array methods are synchronous. If you pass an async function to an Array method, it will not wait for the async function to finish. Loops don’t rely on callbacks, so if you’re already in an asynchronous context, you can await in the loop block or use a for await...of loop.

Consider the example below. This is a fire-and-forget, where the async function passed to Array.prototype.forEach is called, but forEach doesn’t wait for the function to finish.

const urls = [
  'https://example.com',
  'https://timseverien.com',
];

urls.forEach(async () => {
  const response = await fetch(url);

  if (!response.ok) {
    console.warn(`Unable to fetch ${url}`);
  }
});

In the example below, we pass an asynchronous function to an Array.prototype.map call. Because an async function always returns a Promise, the return value of the map call is a list of Promises we can pass to Promise.all, Promise.allSettled, or Promise.race. This way we can run various asynchronous tasks in parallel and wait for the result.

async function assertPageIsOk(url) {
  const response = await fetch(url);

  if (!response.ok) {
    throw new Error(`"GET ${url}" responded with ${response.statusCode}`);
  }

  return true;
}

const urls = [
  'https://timseverien.com',
  'https://timseverien.com/posts',
];

try {
  await Promise.all(urls.map(assertPageIsOk));
} catch (error) {
  console.error(error);
}

Let’s make the previous example a little bit more complex. Instead of firing all requests at the same time, possibly causing a denial of service, let’s call each URL sequentially. We can stop the sequence if one of them returns a non-2xx status code.

function assertPagesAreOk(urls) {
  return urls.reduce(async (chain, url) => {
    await chain;

    return assertPageIsOk(url);
  }, Promise.resolve());
}

assertPagesAreOk(urls)
  .then(() => ...)
  .catch(() => ...);

Here’s the loop equivalent of the above:

async function assertPagesAreOk(urls) {
  for (const url of urls) {
    await assertPageIsOk(url);
  }

  return true;
}

assertPagesAreOk(urls)
  .then(() => ...)
  .catch(() => ...);

In all of the examples above, I find the loops equally or just a bit more readable.

The second significant difference between loops and Array methods is arrays can’t be infinitely big. Normally we would consider infinite loops a bad thing, but they can be very useful.

Imagine we’re working on a modern scatter plot library in which we want to draw a massive number of points in a canvas. Because arrays are limited in size, we can leverage iterators by allowing users to pass a generator function.

import createScatterPlot from 'scatman';

createScatterPlot({
  data: *function() {
    while (true) {
      yield {
        x: Math.random(),
        y: Math.random(),
      };
    }
  },
});

A generator function returns a generator object, which is a superset of an iterator. These can either be converted to an array (via [...iterator] or Array.from(iterator)) or used in a loop. As I mentioned before, the max size of an array is a limitation. In a loop, on the other hand, individual values are pulled out of the iterator when the loop needs the next value. Because we generate infinite points in our example, converting it to an array would freeze the browser.

for (const { x, y } of data) {
  draw(x, y);

  await waitForNextFrame();
}

Conclusion

We’ve learned loops are faster, but Array methods can be more readable when it handles some logic internally, like the creation of a new array or filtering out specific values. We also saw that loops can be more readable when dealing with asynchronous tasks and learned that they are useful for iterating an unknown number of items, as is common with iterators.

In most cases, we use either of them to loop over an array of fewer than one thousand items, in which case we should strongly lean towards the more readable option. I feel that Array methods are more readable, with the exception of Array.prototype.reduce. Too often I see front-enders struggle with that method, not fully understanding its behaviour.

Regardless, no one knows you, your team, or your project better than you do. Maybe you’re working in a team of full-stackers who are more familiar with loops than they are with Array methods. Perhaps the Array methods are more in line with your functional programming philosophy. You (and your team) will have to figure that out.

Did you like this post? Want to pick my brain? Say hi on Twitter!