[go: up one dir, main page]

DEV Community

Cedric
Cedric

Posted on • Edited on

JavaScript Arrays: .forEach - The Functional for of loop

Meet Array​.prototype​.for​Each().

It's purpose is to execute the code you provide it over each item of the array: essentially a loop.

Here's its definition:

array.forEach(function callback(currentValue [, index [, array]]) {  
// code for this iteration  
}[, thisArg]);  

Let's explain it below. 😉

It's Parameters

forEach accepts up to two parameters:

  • the callback function, which is executed over each item of the array
  • the thisArg (optional), which changes the value of this inside of the callback function

Now, a deeper look at each one. 👇

1) The callback function

The method that is called on each element of the array. It takes up to three parameters:

  • currentValue: current item of the array, 🍎 on the first iteration
  • index (optional): index of the current item, 0 on the first iteration
  • array (optional): the whole array, the same on all iterations
const array = ["🍎", "🍌", "🍍"];

array.forEach(function(current, index, array) {  
console.log(current);  
console.log(index);  
console.log(array);  
console.log("\n");  
});

// LOGS  
// { current: '🍎' }  
// { index: 0 }  
// { array: [ '🍎', '🍌', '🍍' ] }  
//  
// { current: '🍌' }  
// { index: 1 }  
// { array: [ '🍎', '🍌', '🍍' ] }  
//  
// { current: '🍍' }  
// { index: 2 }  
// { array: [ '🍎', '🍌', '🍍' ] }  

The index Parameter

The index parameter is optional. It is handy when the logic depends on the item's position in the array.

const fruitEmojis = ["🍎", "🍌", "🍍"];  
const fruitNames = ["apple", "banana", "pineapple"];

fruitEmojis.forEach(function logFruitName(currentFruitEmoji, index) {  
const fruitName = fruitNames[index];

console.log({ emoji: currentFruitEmoji, name: fruitName });  
});

// LOGS  
// { emoji: '🍎', name: 'apple' }  
// { emoji: '🍌', name: 'banana' }  
// { emoji: '🍍', name: 'pineapple' }  

The array Parameter

The last parameter is array. It is the value of the whole array at the beginning of the execution of the callback method.
Handy: It is useful when you have a generic method that you pass to forEach which requires access to the array.

The method being generic you cannot know in advance the array it will be called on. Which also mean you cannot rely on the closure as it is unknown.

Thus the array parameter in that case is your only option to get access to the current array.

See this Stackoverflow response reply from redneb for more info.

2) The thisArg Parameter

Overrides the this keyword value inside the callback function.

By default this would refer to the window object, and it will be overridden by the value you pass it.

const array = [1];

array.forEach(function basicCall(current) {  
console.log(this);  
});

// LOGS  
// ... the whole Window object actually (in the browser)

const thisArg = { context: "custom" };

array.forEach(function explicitThisArg(current) {  
console.log(this);  
}, thisArg);

// LOGS  
//`{context: "custom"}`, which is the custom `this` value passed  

My Usage of forEach

I generally use it when I want to apply a side-effect to some object or another array. (I try avoiding side-effect as much I can.)

Example

In this case we have a list of emojis and the corresponding list of names. We want to create an object where the key will be the name and the value will contain the emoji.

The two arrays are sorted the same way: at any given index items from both array correspond.

const fruitEmojis = ["🍎", "🍌", "🍍"];  
const fruitNames = ["apple", "banana", "pineapple"];

let fruitMap = {};

fruitEmojis.forEach(function addKeyPairToFruitMap(currentFruitEmoji, index) {  
const key = fruitNames[index];

fruitMap[key] = currentFruitEmoji;  
});

console.log(fruitMap);

// LOGS  
// { apple: "🍎", banana: "🍌", pineapple: "🍍" }  

Note that fruitMap is created before calling forEach on fruitEmojis. And we update the object during the executions of addKeyPairToFruitMap.

Information To Consider

Before using the forEach methods on arrays here's some information worth knowing.

1) Returns undefined, thus NOT Chainable

The forEach array method always returns undefined, thus it is NOT chainable. Which means that in a call chain, it can only be placed at the end.

const fruitEmojis = ["🍎", "🍌", "🍍"];

let fruitMap = {};

fruitEmojis  
.forEach(function addKeyPairToFruitMap(currentFruitEmoji) {  
return currentFruitEmoji;  
})  
.map(function logEmoji(emoji) {  
console.log("Calling `.map` will throw an error!");  
}  
);

// LOGS (console.error)  
// ... (omitted details)  
// .map(function logEmoji(emoji) {  
// ^  
// TypeError: Cannot read property 'map' of undefined  
// ... (omitted details)  

2) Callback Function Can Modify Original Array

We can add/remove/update items from the array from inside the callback function.

Addition

Adding does NOT affect the items for the call: only the items initially present are processed.

But after the execution we see that it was affected.

const fruitEmojis = ["🍎", "🍌", "🍍"];

let fruitMap = {};

fruitEmojis.forEach(function addKeyPairToFruitMap(currentFruitEmoji, index) {  
fruitEmojis.push(`test ${index}`);

console.log({index});  
});

console.log({fruitEmojis});

// LOGS

// `forEach`:  
// { index: 0 }  
// { index: 1 }  
// { index: 2 }

// logging the array:  
// { fruitEmojis: [ '🍎', '🍌', '🍍', 'test 0', 'test 1', 'test 2' ] }  

Deletion

Deletion DOES affect the number of items for the call. If the next planned item is removed it will not be processed.

let fruitEmojis = ["🍎", "🍌", "🍍"];

let fruitMap = {};

fruitEmojis.forEach(function addKeyPairToFruitMap(currentFruitEmoji, index) {  
fruitEmojis.shift();  
fruitEmojis.pop();  
fruitEmojis.splice(0, 1);

console.log({index});  
});

console.log({fruitEmojis});

// LOGS

// `forEach`:  
// { index: 0 }

// logging the array:  
// { fruitEmojis: [] }  

Modification

Modification DOES affect the items themselves for the call, but not the count. If we modify the next planned item, this modification is available when it is then processed.

Note that the forth item is due to this statement the following statement which adds an item to the array on the last execution: fruitEmojis[index + 1] = "AAAAAARH!";.

let fruitEmojis = ["🍎", "🍌", "🍍"];

let fruitMap = {};

fruitEmojis.forEach(function addKeyPairToFruitMap(currentFruitEmoji, index) {  
fruitEmojis[index + 1] = "AAAAAARH!";  

console.log({currentFruitEmoji, index});  
});

console.log({fruitEmojis});

// LOGS

// `forEach`:  
// { currentFruitEmoji: '🍎', index: 0 }  
// { currentFruitEmoji: 'AAAAAARH!', index: 1 }  
// { currentFruitEmoji: 'AAAAAARH!', index: 2 }

// the array  
// { fruitEmojis: [ '🍎', 'AAAAAARH!', 'AAAAAARH!', 'AAAAAARH!' ] }  

3) Cannot Be Stopped

You cannot stop the execution or "break the loop" when calling the forEach method.

If you are trying to stop the execution you should probably use a different array method (eg. find, filter, reduce, some, includes) or use a for-loop instead.

Conclusion

I hope this article about the forEach method has brought you value. 🙂

It will be part of a series on JavaScript arrays, so stay tuned for the next one! 🎉

Until then, happy coding! 😎

Top comments (7)

Collapse
 
drozerah profile image
Drozerah • Edited

"Cannot Be Stopped" => It's possible to throw an exception to break the method if you really need, see that stackoverflow post

Thank you for the this reminder in the article !

Collapse
 
cedpoilly profile image
Cedric

Agreed ;) Thank you for mentioning this. And throwing an exception can stop any kind of code.
Then this technique is a hack, it is not according to the design of the language like the break statement for imperative loops. Nor is it a feature of the forEach loop.
So throwing an exception in a forEach callback is possible, but when you reach that point in your code, refactoring to a for of or a for i loop would actually improve readability. :)

Collapse
 
drozerah profile image
Drozerah

Yes totally agree as mentioned - though, we can wondering why such a break thing is not available for this method ? I mean, particularly, in the way to have a better understanding of the method itself and JavaScript more generaly...

Collapse
 
mikegeyser profile image
mike geyser ⟨ 🐘 ⁄ ⟩

Thanks for the great read! It's always interesting seeing other peoples perspectives on it.

I feel like forEach is in this weird space between for of and the more functional operators map/reduce. Almost every use case for looping over an array is to change its shape, either by manipulating each item (which makes map ideal) or by aggregating the array contents (which makes reduce ideal).

Using your key/value map example, I would typically use reduce as follows:

const fruitEmojis = ['🍎', '🍌', '🍍'];
const fruitNames = ['apple', 'banana', 'pineapple'];

let fruitMap = fruitEmojis.reduce(function(map, currentFruitEmoji, index) {
  const key = fruitNames[index];
  map[key] = currentFruitEmoji;

  return map;
}, {});

console.log(fruitMap);

It's very similar, but has notably fewer side effects. You can also make it really terse, if that's your kind of thing, and if you use the spread operator to shallow copy you can reduce the footprint for unintentional side effects even further:

let fruitMap = fruitEmojis.reduce((map, current, i) => ({ 
    ...map, 
    [fruitNames[i]]: current 
}), {});

Just some thoughts. :)

Collapse
 
cedpoilly profile image
Cedric

Thanks for this insightful explanation @mikegeyser !

It's true that if I'm using .forEach quite often, it's worth considering using more "specialised" methods such as .reduce.

Which is a great transition to announce the topic for the next article of this series: Array.prototype.reduce!

Stay tuned! ✌

Collapse
 
vasil9v profile image
Vasil Daskalopoulos

Great article! Small typo in the description of the callback: "1 on the first iteration" - should be 0 on the first iteration :)

Collapse
 
cedpoilly profile image
Cedric

Thanks @vasil9v ! Fixed :)