はじめに
この記事はJavaScript
のforEach
の仕様について書いています。
開発作業中に調べる頻度が高いと思ったので、一度しっかり理解しておこうと思い記事を書きました。同じような方は参考にしていただけますと幸いです。
公式ドキュメントを参考にして書いています。
forEachとは
念の為、forEach
について確認しておきたいと思います。
forEach() メソッドは反復処理メソッドです。指定された関数 callbackFn を配列に含まれる各要素に対して一度ずつ、昇順で呼び出します。 map() と異なり、 forEach() は常に undefined を返し、連鎖させることはできません。典型的な使用する用途は、チェーンの終わりで副次効果を実行することです。
上記は公式ドキュメントの一部を引用したものです。
配列の要素数の数だけループを回すメソッドで、返り値はありません。
下記のように配列に対してメソッドチェーンで繋げて使用します。
const ary = [1, 2, 3]
ary.forEach((x) => {
console.log(x)
})
// 出力結果
// 1ループ目: 1
// 2ループ目: 2
// 3ループ目: 3
forEachの仕様
次の6つについての説明していきたいと思います。
- ループを止める方法について
- ループ対象に空の要素があるとスキップされる
- ループ中にループ対象の要素数が増えた場合にどうなるか
- ループ中にループ対象の要素を変更した場合にどうなるか
- 非同期関数の終了を待たない
- オブジェクトをループ対象とする場合はどうすればいいか
ループを止める方法について
どうすればforEachのループが止まるかについて説明していきます。
まず結論ですが下記のようになります。
アクション | ループが止まるかどうか |
---|---|
break | ✖️ |
continue | ✖️ |
return | ◯ |
例外処理発生 | ◯ |
forEachではbreak
やcontinue
でループを止めることはできません。
ループを止めるにはreturn
を使います。
const ary = [1, 2, 3]
ary.forEach((x) => {
if (x === 1) {
return
}
console.log(x)
})
// 出力結果
// 1ループ目: 出力なし
// 2ループ目: 2
// 3ループ目: 3
return
はfor
文でいうとcontinue
のような動きにあたります。
それとループを止めるには下記のようにtry..catchを使わずに例外処理を発生させるという方法もあります。
const ary = [1, 2, 3]
ary.forEach((x) => {
if (true) {
throw "Error Text"
}
console.log(x)
})
エラーメッセージ
Uncaught Error Text
この場合forEach
だけでなく全プロセスが終了する可能性もあります。
ループ対象に空の要素があるとスキップされる
「空」という言葉が少し曖昧なので説明させていただきますと、下記のような配列のことです。
const ary = [1, , 3]
空文字やundefined
のことではなく、配列に何も定義されていな状態のことです。
forEach
を使って出力すると下記のような結果になります。
const ary = [1, , 3]
ary.forEach((x) => {
console.log(x)
})
// 出力結果
// 1ループ目: 1
// 2ループ目: スキップされる
// 3ループ目: 3
ちなみにundefined
を設定するとスキップされずに出力されます。
const ary = [1, undefined, 3]
ary.forEach((x) => {
console.log(x)
})
// 出力結果
// 1ループ目: 1
// 2ループ目: undefined
// 3ループ目: 3
ループ中にループ対象の要素数が増えた場合にどうなるか
forEach
内でループ対象の配列に対して、要素を追加してもその分は
ループ対象には入りません。
const ary = [1, 2, 3]
ary.forEach((x) => {
// 1ループ目に配列の要素を追加する
if (x === 1) {
ary.push(4)
}
console.log(x)
})
console.log(ary)
// 出力結果
// 1ループ目: 1
// 2ループ目: 2
// 3ループ目: 3
// [1, 2, 3, 4] ← ループの外のconsole.log()では要素が追加されている
出力結果を見ていただくと、配列に要素を追加しているにもかかわらず、3ループ目でforEach
が終了していることがわかると思います。
これはforEach
開始時点での要素数の分だけループを回すためです。
ループ中にループ対象の要素を変更した場合にどうなるか
下記のコードの通り、ループ前の要素であれば変更が適用されます。
const ary = [1, 2, 3]
ary.forEach((x) => {
// 1ループ目に配列の要素を変更する
if (x === 1) {
ary[2] = 4
}
console.log(x)
})
// 出力結果
// 1ループ目: 1
// 2ループ目: 2
// 3ループ目: 4
forEach
は開始時点の配列の要素数のみを保存して、その要素の内容については特に保存していないようです。
非同期関数の終了を待たない
async
とawait
を使って非同期処理を同期的に実行しようとした際などに、forEach
は非同期処理の終了を待ちません。
const ratings = [5, 4, 5]
let sum = 0
const sumFunction = async (a, b) => a + b
async function foo() {
ratings.forEach(async (rating) => {
sum = await sumFunction(sum, rating)
})
console.log(sum)
}
foo()
// 出力結果
// 0
上記のコードは公式ドキュメントから引用したものです。
非同期関数の終了を待つ場合は結果が14になるはずですが、0が出力されてしまいます。
ちなみにこの問題はfor
を使うことで解決できます。
const ratings = [5, 4, 5]
let sum = 0
const sumFunction = async (a, b) => a + b
async function foo() {
for (let i = 0; i < ratings.length; i++) {
sum = await sumFunction(sum, ratings[i])
}
console.log(sum)
}
foo()
// 出力結果
// 14
オブジェクトをループ対象とする場合
オブジェクトをforEach
のループで回す方法について説明します。
const obj = { length: 2, 0: 0, 1: 1 }
Array.prototype.forEach.call(obj, (x) => console.log(x))
// 出力結果
// 1プール目:0
// 2プール目:1
上記のようにArray.prototype.forEach.call
を使ってオブジェクトをループで回すことができます。オブジェクトにはlength
キーが含まれている必要があり、そこに設定された数値の分だけループが繰り返されます。
また、キー名は数値である必要があります。
オブジェクトをループで回すのにforEach
は少し使い勝手が悪いと思いますので、他の方法がいいかと思います。
終わりに
最後まで読んでいただきありがとうございました。
普段使っているメソッドもしっかり調べると知らなかった仕様がたくさんあるもんだなと思いました。
参考