Exploring the JavaScript forEach Method for Looping Over Arrays

Jack Misteli

Array.prototype.forEach is a nice little function introduced in ECMAScript 2015. It allows us to access each element of an array in order.

The basics

Most of us know the alphabet so here’s an easy example:

Module: letters.js

const letters = ['a', 'b', 'c'];

letters.forEach((letter, index, arr) => {
  console.log(letter,index, arr);
});

// The console will output
// 'a', 0, ['a', 'b', 'c']
// 'b', 1, ['a', 'b', 'c']
// 'c', 2, ['a', 'b', 'c']
Learn JavaScript basics for free

Under the hood

Here is a rough JavaScript implementation of the callback flow as described in the ECMAScript documentation.

Module: myForEach.js

const myForEach = (array, callback) => {
  // Before iterating through the array forEach checks the value of array and sets a len variable
  let k = 0;
  // If the argument passed doesn't have a property len then forEach returns
  if(!array.length)
    return;
  //  checking if callback is callable
  if (typeof callback != 'function')
    return;
  // The user can set a custom this context
  let len = array.length;
  
  // iterating until k reaches the length of the array - 1
  while(k<len){
    // if the array doesn't have a k element at index k then we return
    if(!array[k]){
      return;
    }
    let element = array[k];

    // notice the three elements used in the callback
    callback(element, k, array);

    // Increase k to reach the next item in the array
    k += 1;
  }
  // forEach never returns anything (return undefined is the same as return)
  return undefined;
};

Modifying the Original Array

As you can see from myForEach implementation, we get the value of element by assignment:

Module: myForEach.js

let element = array[k];

So what happens if we modify element?

const ruinYourElements = (element, index) => {
  element = '乁( ◔ ౪◔)「 ';
}

const verySeriousArray = ['business', 'files', 'documents']

verySeriousArray.forEach(ruinYourElements)
// verySeriousArray =  ['business', 'files', 'documents']`
// You failed to ruin my array

In this snippet, element goes from referencing array[k] to referencing '乁( ◔ ౪◔)「 '. array[k] never knows about that reassignment.

BUT! things are different with objects!

const ruiningYourNames = (element, index) => {
  element.name = '乁( ◔ ౪◔)「 ';
}

const verySeriousArray = [{name:'business'}, {name:'files'}, {name:'documents'}];

verySeriousArray.forEach(ruiningYourNames);
// verySeriousArray =  [{name: '乁( ◔ ౪◔)「 '}, {name: '乁( ◔ ౪◔)「 '}, {name: '乁( ◔ ౪◔)「 '}]
// You succeeded at ruining my array

The changes occur because element still references array[k]. If we wanted to prevent such a behavior we would have to deep clone array[k] in myForEach:

Module: myForEach.js

if(typeof array[k] === 'object'){
  let element = JSON.parse(JSON.stringify(array[k]));
}

If you want to change the value of an element in the original array you have to modify the third argument of the forEach callback: arr:

const ruinYourArray = (element, index, arr) => {
  arr[index] = '乁( ◔ ౪◔)「 ';
}

const verySeriousArray = ['business', 'files', 'documents']

verySeriousArray.forEach(ruinYourArray)
// verySeriousArray = ['乁( ◔ ౪◔)「 ', '乁( ◔ ౪◔)「 ', '乁( ◔ ౪◔)「 ']
// We successfully ruined the serious array, nobody will be able to do serious business anymore

How the Loop Works

forEach will iterate for as long as the initial array’s length. If the array is 5 items long, it will iterate 5 times, no more.

Module: stickToYourDietPlan.js

const reasonableShoppingList = ['🍈', '🥗'];
reasonableShoppingList.forEach((item)=> {
  // Here is the 10 year old in me trying to highjack my health
  reasonableShoppingList.push('🥞');
  console.log(`bought ${item}`);
})
// console will output:
// bought 🍈 bought 🥗 
// because forEach called the callback reasonableShoppingList.length = 2 times
// at the end reasonableShoppingList = ['🍈', '🥗', '🥞', '🥞'] so make sure to clean your array before you go shopping again!

The iterations can be interrupted early in two main situations:

1. We reached a point of the array which doesn't exist anymore.

Module: letters.js

const pop =  (letter, index, arr) =>{
  console.log(letter, i);
  arr.pop();
}

letters.forEach(pop);
// 'a'
// 'b'
// letters = 'a'

Be careful when you modify arrays! Sometimes you will have some counterintuitive results:

Module: letters.js

letters.forEach((letter, index, arr)=>{
  console.log(letter, index);
  if (letter === 'a')
    arr.shift();
});
// 'a' 0
// 'c' 1
// letters = ['b','c']

Checkout myForEach, think about it, and it should make sense.

2. If the callback function crashed

const showCity = (user) => {
  console.log(user.address.city);
}

const users = [
  {
    name:'Sarah',
    address:{
      zipCode: 60633,
      city: 'Chicago'
    }
  },
  {
    name:'Jack'
  },
  {
    name:'Raphael',
    address: {
      city: 'ParadiseCity'
    }
  }
];

users.forEach(showCity);
// Console will output: 'Chicago'.Then we'll get:
// Uncaught TypeError: Cannot read property 'city' of undefined

Using forEach in Legacy Browsers

There are still users using legacy browsers which do not support forEach. For them, your safest bet is to use for loops. But if you want to be able to use all ECMA2015 functionalities, you should use a polyfill or es5 shims.

forEach() vs map()

As you can see in myForEach, forEach always returns undefined and map returns a new array.

Asynchronous forEach

If you enjoy coding with async and await you might not get the behaviors you expect.

Module: cheeseShopping.js

  // We are going to the cheese shop and ask the vendor what cheese we need for our dish
const cheeseShopping = async (dishes) => {
  const whatCheeseShouldIUse = async (dish) => {
    // We use setTimeout to simulate an API call
    await new Promise(resolve => setTimeout(resolve, 200));

    switch (dish) {
      case 'Pasta':
        return  'Parmesan'
      case 'Gratin':
        return  'Gruyère'
      case 'Cheeseburger':
        return  'American Cheese'
      default:
        return  'Tomme'
    };
  };

  const requiredCheeses = [];

  dishes.forEach( async (dish) => {
    const recommendation = await whatCheeseShouldIUse(dish)
    //  We never reach this code because foreach doesn't wait for await and goes to the next loop
    requiredCheeses.push(recommendation)
  })
  // requiredCheeses = [] 
  
  // this await is useless
  await dishes.forEach( dish => {
    const recommendation =  whatCheeseShouldIUse(dish);
    // Is a promise so we push a promise and not the result of the promise
    requiredCheeses.push(recommendation);
  });
  //requiredCheeses = [Promise, Promise, Promise]
  };
}

const dishes = ['Pasta', 'Cheeseburger', 'Original Cheese Platter'];
cheeseShopping(dishes);

We need to create a custom asyncForEach which waits for each promise to resolve before it moves on. Here’s an example:

Module: blockingAsyncForEach.js

Array.prototype.asyncForEach = async function (callback) {
  let k = 0;
  while (k < this.length) {
    if(!this[k])
      return;
    let element = this[k];
    // This will pause the execution of the code
    await callback(element, k, this);
    k += 1;
  };
};

Arrow Functions explains why we need to use function instead of arrow functions.

const cheeseShopping = async (dishes) => {
  // ... Skipping some code
  await dishes.asyncforEach( async dish => {
    const recommendation =  await whatCheeseShouldIUse(dish);
    requiredCheeses.push(recommendation);
  })
  //requiredCheeses = ['Parmesan', 'American Cheese', 'Tomme']
  
  return requiredCheeses;
};

Sometimes (often?) you might want to run all async functions at the same time and awaits for all of them to resolve. Promise.all() could be very useful in that situation.

Performance

forEach loops are slower than a classic for loop but we are talking microseconds over an array of a million elements so don’t worry about it. Interestingly, the relative performance of map and forEach depends on the version of the browser you’re using, Chrome 61.0.3135 (2) has a faster map Chrome 61.0.3136 (1) has a faster forEach.

Graph chart with performance comparison

The DOM Trap

Be careful! Not everything that looks like an array is an array:

const divs = document.getElementsByTagName('div');
divs.forEach(doSomething);
// Uncaught TypeError: divs.forEach is not a function

That’s because divs is not an array! It is a special object called a DOMCollection which is an iterable object. So you can only do:

for (let i = 0; i < divs.length; i++){
  doSomething(divs[i], i);
}

Or mess with HTMLCollection’s prototype and add a forEach to force it to behave like the native forEach:

HTMLCollection.prototype.forEach = Array.prototype.forEach;

Or just make the array-like object into a real array first using something like the spread operator:

const divs = document.getElementsByTagName('div');
const divsArr = [...divs];
// ...
  Tweet It

🕵 Search Results

🔎 Searching...

Sponsored by #native_company# — Learn More
#native_title# #native_desc#
#native_cta#