3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

JavaScript forEachを何度も調べがちな方々へ

Last updated at Posted at 2024-01-06

はじめに

この記事はJavaScriptforEachの仕様について書いています。
開発作業中に調べる頻度が高いと思ったので、一度しっかり理解しておこうと思い記事を書きました。同じような方は参考にしていただけますと幸いです。

公式ドキュメントを参考にして書いています。

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つについての説明していきたいと思います。

  1. ループを止める方法について
  2. ループ対象に空の要素があるとスキップされる
  3. ループ中にループ対象の要素数が増えた場合にどうなるか
  4. ループ中にループ対象の要素を変更した場合にどうなるか
  5. 非同期関数の終了を待たない
  6. オブジェクトをループ対象とする場合はどうすればいいか

ループを止める方法について

どうすればforEachのループが止まるかについて説明していきます。
まず結論ですが下記のようになります。

アクション ループが止まるかどうか
break ✖️
continue ✖️
return
例外処理発生

forEachではbreakcontinueでループを止めることはできません。
ループを止めるにはreturnを使います。

const ary = [1, 2, 3]
ary.forEach((x) => {
  if (x === 1) {
    return
  }
  console.log(x)
})

// 出力結果
// 1ループ目: 出力なし
// 2ループ目: 2
// 3ループ目: 3

returnfor文でいうと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は開始時点の配列の要素数のみを保存して、その要素の内容については特に保存していないようです。

非同期関数の終了を待たない

asyncawaitを使って非同期処理を同期的に実行しようとした際などに、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は少し使い勝手が悪いと思いますので、他の方法がいいかと思います。

終わりに

最後まで読んでいただきありがとうございました。
普段使っているメソッドもしっかり調べると知らなかった仕様がたくさんあるもんだなと思いました。

参考

3
1
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?