The deepest circles of JavaScript hell are reserved for people that use forEach
.
krakenTentacles.forEach(tentacle => tentacle.mutate()); // uh-oh!
To understand why this cursed method exists, we need to look at the original ways to loop over an array.
The safe option was the for
loop.
for (var i = 0; i < krakenTentacles.length; i++) {
krakenTentacles[i].mutate();
}
And the other was the for..in
loop.
for (var i in krakenTentacles) {
krakenTentacles[i].mutate();
}
It may look friendlier but for..in
has some big problems.
At the time, var
was the only way to define a local variable. This meant that both loop's variables would be visible outside of the loop block, which caused all kinds of bugs.
And lo' and behold, the fifth edition of ECMAScript appeared with a method for safely iterating over arrays: forEach
!
krakenTentacles.forEach(function(tentacle, i) {
console.log(i, number);
});
This seems like a reasonable way to solve some legitimate problems without changing the grammar of the language.
However... JavaScript has improved a lot since forEach
first appeared. Most of these problems have better solutions, and the language has grown in ways that forEach
doesn't understand.
forEach
doesn't cut it any more, and you shouldn't be using it in new code!
You can't return
a value early.
function findLoadedCannon(cannons) {
cannons.forEach(cannon => {
if (cannon.isLoaded()) {
return cannon; // Has no effect on the outer function.
}
});
}
escapeRoutes.forEach(escapeRoute => {
if (escapeRoute.isOpen()) {
break; // SyntaxError
}
});
async function prayForDeliverance(gods) {
gods.forEach(god => {
let answer = await prayTo(god); // SyntaxError
callOut(answer);
});
}
forEach
regularly causes confusion in asynchronous code, where it looks like you can solve the problem by making the callback function async
too.
async function prayForDeliverance(gods) {
let answers = [];
await gods.forEach(async god => {
let answer = await prayTo(god);
answers.push(answer);
});
return answers;
}
forEach
quietly ignores the async
nature of the callback, and await
quietly ignores the undefined
that forEach
returns.
prayForDeliverance
will return a promise that always resolves to an empty array.
Then at some point in the future, the fetch
calls start to resolve in an unpredictable order, and that array starts to mutate underneath you.
You're dealing with race conditions, asynchronous mutations, and unhandled promise exceptions in this little function, all because you used forEach
.
You can't use forEach
to iterate over a string
.
"shipwreck".forEach();
// undefined is not a function
new Map([[3, "buckets"], [2, "ropes"]]).forEach();
// undefined is not a function
new Set(["spyglass", "compass", "rum"]).forEach();
// undefined is not a function
Or any other third party data structure that implements the iterable protocol.
import { PegLegTree } from "@pirates/trees";
new PegLegTree(23, 54, 64, 12).forEach();
// Only works if `PegLegTree` has implemented a `forEach` method.
forEach
actually tried to solve this problem before iterables existed!
When forEach
appeared, arrays were not the only thing people needed iterate over. The language designers knew this, and they included the following note in the specification:
The
forEach
function is intentionally generic; it does not require that its this value be an Array object.Therefore it can be transferred to other kinds of objects for use as a method. Whether the
forEach
function can be applied successfully to a host object is implementation-dependent.
Here's an example of "transferring" forEach
to a string.
Array.prototype.forEach.call("shipwreck", char => {
console.log(char);
});
It works (in an implementation-dependent sense), but it's not pleasant. It's not obvious why the prototypal inheritance model is leaking out into the code.
Behind the scenes it is checking for a numeric length
property, then attempting to iterate over the indexes.
This means that you get some interesting behaviours when you call forEach
on an object that's pretending to be an array.
// Don't run this unless you want to crash your browser
Array.prototype.forEach.call({ length: Infinity }, console.log);
forEach
is a sinking ship that we need to abandon. Thankfully, for..of
is our lifeboat!
for (let tentacle of krakenTentacles) {
tentacle.mutate();
}
You can return
from inside.
function findLoadedCannon(cannons) {
for (let cannon of cannons) {
if (cannon.isLoaded()) {
return cannon;
}
});
}
for (let escapeRoute of escapeRoutes) {
if (escapeRoute.isOpen()) {
break;
}
});
async function prayForDeliverance(gods) {
for (let god of gods) {
let answer = await prayTo(god);
callOut(answer);
}
}
The for..of
loop works with any object that implements the Iterable protocol (including Array
, String
, Map
and Set
).
It also works with let
and const
to create block scoped variables that are only visible inside the loop.
You can fall back to a for
loop if you need the index variable too.
forEach
was the short-term fix, but for..of
is the long-term solution!