Monday, 18 November, 2019 UTC


Summary

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'] 
ⓘ About this sponsored banner
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.
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]; // ...