背景
JavaScriptでは forEach
という配列から要素を取り出して反復処理できる関数があります。
前の記事 JavaScriptのforEach内でbreakができない理由【備忘録】 では、 forEach
の中では break
が使えず、途中でループを抜けることができない理由についてまとめました。
今回はその続きとして、forEach
の内部で await
を使った場合に、非同期処理の完了を待たずに次の処理へ進んでしまう、という挙動について理由を調べてみました。
forEach内でawaitしてみる
forEach
の中で await
して得られた値を配列に追加し、 forEach
の直後に配列を console.log
で出力する例で確認します。
for文の感覚でいえば、イテレーション中の await
で都度処理を待ってくれるイメージなので、全ての要素への処理が終わったら配列の各要素が出力されると予想できます。
// 楽器の名前を文章に変換する非同期関数
function describeInstrument(instrument) {
return new Promise(resolve => {
setTimeout(() => {
resolve(`${instrument} は素晴らしい楽器です!`)
}, 1000)
})
}
const instruments = ['ドラム', 'スラップフォン']
const descriptions = []
instruments.forEach(async item => {
const result = await describeInstrument(item)
descriptions.push(result)
})
console.log('forEach終了後のdescriptions: ', descriptions)
// 実行結果:
// forEach終了後のdescriptions: []
結果は、forEach
内で await
してから配列に値を追加しているのにも関わらず、直後のconsole.log
では空の配列が出力されています。
awaitについて(前提知識)
await
は、 Promise
を返す関数の非同期処理が完了するのを待ち、その結果の値を取得するために使います。
非同期処理が完了するまでは該当の関数の実行は一時停止され、その間は他の処理(UI の更新や他の関数の実行など)ができます。
await と Promise の関係
Promise
を返す関数は、実行された直後にはまだ結果がわからないため、まず「未解決の Promise
(Pending 状態)」を返します。
その Promise
が解決(resolve)されるとawait
は結果の値を受け取り、中断された関数が再開されます。
await
しない場合は非同期処理の解決を待たないため、未解決の Promise
のみ返却されて処理が進みます。
forEachの定義(前提知識)
for文は構文として定義されていますが、forEach
はメソッドです。
ECMA-262の forEach
の仕様では、対象の配列をループさせて forEach
内に書いた関数をコールバック関数として呼び出しています。
1. Let O be ? ToObject(this value).
2. Let len be ? LengthOfArrayLike(O).
3. If IsCallable(callback) is false, throw a TypeError exception.
4. Let k be 0.
5. Repeat, while k
なぜ forEach は非同期処理を待ってくれないのか
forEach
が Promise
を返すコールバック関数の実行を待ってくれない理由は、 forEach
のループ内でコールバック関数を await
していないためです。
awaitについて で述べたように、Promise
を返すコールバック関数を呼び出すと、一旦未解決の Promise
が返ってきます。
forEachの定義 を確認すると、forEach
内ではコールバック関数をループの中で呼び出していますが、 await
がないため、Promise
の解決を待たないままループが進行します。
これにより forEach
はコールバック関数の結果を待たずに処理を続行してしまいます。
補足
ただし、Promise
の結果を forEach
のループ内で待ってくれないだけで、コールバック関数の処理自体は実行されています。
そのため、十分な時間を待った後にコールバック関数内で更新された配列を確認すると、値が入っていることが確認できます。
function describeInstrument(instrument) {
return new Promise(resolve => {
setTimeout(() => {
resolve(`${instrument} は素晴らしい楽器です!`)
}, 1000)
})
}
const instruments = ['ドラム', 'スラップフォン']
const descriptions = []
instruments.forEach(async item => {
const result = await describeInstrument(item)
descriptions.push(result)
})
// Promise が解決する前の出力
console.log('forEach終了後のdescriptions:',descriptions)
// Promise が解決した後の出力
setTimeout(() => {
console.log('3秒後のdescriptions:', descriptions)
}, 3000)
// 実行結果:
// forEach終了後のdescriptions: []
// 3秒後のdescriptions: ["ドラム は素晴らしい楽器です!", "スラップフォン は素晴らしい楽器です!"]
終わりに
forEach
内で await
をすると、仕様により非同期処理の完了を待たずに処理が進んでしまうことがわかりました。
今回は forEach
内の await
で非同期処理の完了を待たない理由に焦点を当てましたが、for...of
構文を使うことで、配列の各アイテムを非同期処理することができます。forEach
だけでなく普段何気なく使っている関数やメソッドを使う際にも注意したいと思います。
Views: 0