Over the past year of learning programming and software engineering, I came across different ways to perform loops.
A loop is a powerful tool for repeating actions, but that’s not all to it. Today, I will be focusing on JavaScript’s
Array.forEach()
loop and the regular for
loop.
Syntax Differences
The Array.forEach()
loop is a higher-order array function, so it has to be used with arrays. On the other hand,
for
loops are more generic, and can be used for the sole purpose of wanting to repeat something. For meaningful
comparison, we will take a look at the process of iterating through and logging the following array of fruit objects.
const fruits = [
{name: "apple", weight: "100g"},
{name: "banana", weight: "80g"},
{name: "orange", weight: "70g"},
{name: "grape", weight: "200g"}
]
// Basic for loop
for (let i = 0; i < fruits.length; i++) {
console.log(`Name: ${fruits[i].name}, Weight: ${fruits[i].weight}`)
}
// Using forEach loop
fruits.forEach(fruit => console.log(`Name: ${fruit.name}, Weight: ${fruit.weight}`))
While both loops achieve the same outcome, the code is more readable using the forEach
loop. In the for
loop,
an iterator i
is used, and i
also serves as the index of each fruit in the array. However, in the forEach
loop,
a callback function is passed in, which will execute for each fruit
in the fruits
array. As such, the amount
of code written for a forEach
loop is also reduced.
Obvious Differences
However, while there is a slight decrease in readability, for
loops also offer better flexibility, since forEach
loops execute the same callback for every object in the array. This flexibility is particularly useful for data processing
or short-circuiting loops.
In the following example, we try to get all the fruit
s before hitting the first null
object in the fruits
array.
const fruits = [
{ name: "apple", weight: "100g" },
{ name: "banana", weight: "80g" },
{ name: "orange", weight: "70g" },
null, null, null,
{ name: "grape", weight: "200g" }
]
// Basic for loop to add fruits before hitting null
let fruitBowl1 = []
let iterator1 = 0
const getFruit1 = () => {
for (; iterator1 < fruits.length; iterator1++) {
if (fruits[iterator1] === null) {
return
}
fruitBowl1.push(fruits[iterator1])
}
}
getFruit1()
// Using forEach loop to add fruits before hitting null
let fruitBowl2 = []
let iterator2 = 0
const getFruit2 = () => {
fruits.forEach(fruit => {
iterator2++
if (fruit === null) {
return
}
fruitBowl2.push(fruit)
})
}
getFruit2()
console.log(fruitBowl1) // Only has first 3 fruits
console.log("for loop iterations: " + iterator1) // 3 iterations
console.log(fruitBowl2) // Has all fruits
console.log("forEach loop iterations: " + iterator2) // 7 iterations
It can be seen from this example that the return
statement does not terminate the forEach
loop, unlike the
for
loop. In fact, the return
statement only exits the callback function supplied to the forEach
loop,
but does not change the fact that all fruit
s in the array are being processed.
For the record, the above code snippet is only an example to demonstrate behaviours, and is not representative
of how people should use the forEach
loop.
Subtle Differences
In reality, when working with backend systems, it is crucial to know when to use forEach
and for
loops. While
seemingly serving the same purpose, when it comes to API calls for data fetching and processing, forEach
loops
are actually unable to await
for Promises
to be resolved
.
Because of this, there can be unexpected outcomes when a forEach
loop is used to resolve requests involving the
awaiting of Promises
. In some cases, it can be possible for the forEach
to complete iteration before any data
has been fetched or processed, and the remainder of the code below the loop runs.
This can cause issues to the integrity of the application, as other parts of the programme may rely on the fetched data.
This is made apparent using the example below.
Code for simulating API calls which return Promises
Since data fetching APIs are not instantaneous, I have included a 1s and 2s delay respectively for each of the “fetch APIs”.
const getFruitWeight = (index) => {
return new Promise(resolve => {
const fruits = [
{name: "apple", weight: "100g"},
{name: "banana", weight: "80g"},
{name: "orange", weight: "70g"},
{name: "grape", weight: "200g"}
]
setTimeout(() => {
resolve(fruits[index].weight)
}, 1000)
})
}
const getFruitName = (index) => {
return new Promise(resolve => {
const fruits = [
{name: "apple", weight: "100g"},
{name: "banana", weight: "80g"},
{name: "orange", weight: "70g"},
{name: "grape", weight: "200g"}
]
setTimeout(() => {
resolve(fruits[index].name)
}, 2000)
})
}
In this example, I will be attempting to make a bunch of API calls to a simulated backend, to request information about some fruits in the “database”, which contains an array of fruits and their names (N) and weights (W).
Each request will fetch either a fruit’s name (N) or weight (W) at a specific index.
// Required requests
const requests = [
{index: 1, get: "W"}, {index: 2, get: "N"},
{index: 3, get: "N"}, {index: 0, get: "W"}
]
The requests will be processed using either for
or forEach
loop, shown in the functions below.
const makeArrayUsingFor = async () => {
let compiledArray = []
for (let i = 0; i < requests.length; i++) {
if (requests[i].get === "W") {
compiledArray.push(await getFruitWeight(requests[i].index))
} else {
compiledArray.push(await getFruitName(requests[i].index))
}
}
return compiledArray
}
const makeArrayUsingForEach = async () => {
let compiledArray = []
requests.forEach(async (request) => {
if (request.get === "W") {
compiledArray.push(await getFruitWeight(request.index))
} else {
compiledArray.push(await getFruitName(request.index))
}
})
return compiledArray
}
In addition, I will also be using a performance logger to track how long each function call takes.
const { performance } = require('perf_hooks')
const printFetchedArrays = async () => {
let startTime = performance.now()
const forLoopArray = await makeArrayUsingFor()
let endTime = performance.now()
console.log(`For Loop: ${forLoopArray} - time: ${endTime - startTime}ms`)
startTime = performance.now()
const forEachLoopArray = await makeArrayUsingForEach()
endTime = performance.now()
console.log(`For Each Loop: ${forEachLoopArray} - time: ${endTime - startTime}ms`)
}
printFetchedArrays()
After running the above code, the delay caused by the setTimeout
s used can be felt during the
execution of the for
loop, and the results of the data fetching and processing also show that
the requests were being fetched and processed in the correct order.
However, the delay in the execution of the forEach
loop is unnoticeable. It is also apparent that
the data was not fetched and processed at all, meaning that the programme did not wait for the
Promises to be resolved.
The results are as shown, when executed using node.
$ node forEach-vs-for.js
For Loop: 80g,orange,grape,100g - time: 6038.477099999785ms
For Each Loop: - time: 0.2986999973654747ms
Possible Remedies
Since it was mentioned that forEach
loops are unable to return any values or Promises
, there
is nothing to await
, and thus, the above implementation for fetching data failed.
This does not mean that higher-order array functions are unable to perform data fetching from APIs.
If the higher-order array function was able to return Promises
to be resolved, then it can be
used with await
s, which will allow it to fetched and processed accordingly.
In fact, if the order of fetching was irrelevant, a more efficient approach to data fetching
would be to use an Array.map()
function to fire all the requests together, and return an array
of Promise
s as a result. The Promise.all()
method can be used with the array of Promise
s to
resolve them all at once, achieving seemingly parallel execution.
The function to perform the Array.map()
function is shown below.
const makeArrayUsingMap = async () => {
let compiledArray = await Promise.all(requests.map(async (request) => {
if (request.get === "W") {
return await getFruitWeight(request.index)
} else {
return await getFruitName(request.index)
}
}))
return compiledArray
}
By using this implementation instead, the execution time is lowered as the longest fetch took 2 seconds, and since they were resolved in parallel, the overall execution took roughly 2 seconds as well.
$ node forEach-vs-for.js
For Loop: 80g,orange,grape,100g - time: 6025.564899999648ms
Map: 80g,orange,grape,100g - time: 2012.9765999987721ms
In this example, we managed to preserve the order of requests, but it must be noted that this is NOT guaranteed.
Conclusion
It has been 5 months since I last racked my brain this hard outside of school work. This article was actually inspired by the development of the application I am currently working on.
Hopefully this helps anyone who is a JS Developer.
Full code on GitHub: forEach-vs-for.js