背景
社内でJavaScriptの反復処理について話が上がった際に、「forEach
は break
を使って抜けることができない」と聞きました。
なんとなくの理解しか持ち合わせていなかったため、この機会に forEach
の仕様を見ながらどのような動作をするのか確認してみました。
forEachについて
forEach
は配列の各要素に対して、処理を実行するためのメソッドです。
以下のように使うことができます
const favoriteInstruments = ["ドラム", "ギター", "スティールパン"]
favoriteInstruments.forEach((instrument) => {
console.log(`好きな楽器: ${instrument}`)
})
// 出力例:
// 好きな楽器: ドラム
// 好きな楽器: ギター
// 好きな楽器: スティールパン
仕様について
for文は構文として定義されていますが、forEach
はメソッドです。
ECMA-262の 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 < len,
a. Let Pk be ! ToString(𝔽(k)).
b. Let kPresent be ? HasProperty(O, Pk).
c. If kPresent is true, then
i. Let kValue be ? Get(O, Pk).
ii. Perform ? Call(callback, thisArg, « kValue, 𝔽(k), O »).
d. Set k to k + 1.
6. Return undefined.
これを見ると、対象の配列をループさせて forEach
内に書いた関数をコールバック関数として呼び出していることがわかります。
コールバック関数
コールバック関数は別の関数に引数として渡され、その関数の中で後から呼び出される関数です。
「処理の一部を呼び出し元に委ねたい」という時に用いられます。
function greet(name) {
console.log("こんにちは、" + name + "さん!")
}
// greet をコールバックとして使う関数
function processUserInput(callback) {
const name = "太郎"
callback(name) // ここで greet("太郎") が実行される
}
processUserInput(greet)
// 出力: こんにちは、太郎さん!
forEachでbreakが使えない理由
forEach((item) => { ... })
の書き方は、書き心地がfor文に似ているため、一見するとループの中に処理を書いているように思えます。
しかし実際には forEach
は関数であり、(item) => { ... }
の部分は 関数を引数として渡している(=コールバック関数) という形になります。
つまり、以下のように書いているのと本質的には同じです。
function callback(num) {
console.log(num)
}
const array = [1, 2, 3]
array.forEach(callback) // callback を引数として渡している
このように、forEach
の中に書いた処理は「関数の中身」なので、そこで break
を使おうとすると、 ループではなく「普通の関数の中で break
を使った」 ことになってしまいます。
JavaScriptでは、break
はfor文などのループの中でしか使えないため、以下のようなコードはエラーになります。
function callback(item) {
if (item === 2) {
break // エラー
}
console.log(item)
}
const array = [1, 2, 3]
array.forEach(callback)
また、無名関数を直接 forEach
に渡した場合も同じで、関数の中に break
を書いていることになるため、やはりエラーになります。
const array = [1, 2, 3]
array.forEach((item) => {
if (item === 2) {
break // エラー
}
console.log(item)
})
終わりに
今回はECMA-262の仕様から forEach
を理解するアプローチをとってみました。
特に、forEach
に無名関数を渡す場合は、構文が for 文に似ているため、for 文と同じように書けると誤解しやすいと感じました。
forEach
に限らない話ではありますが、仕様を知っていないと思わぬ動作を引き起こす可能性があるため、普段何気なく使っているメソッドや構文についても、「どうしてこのような動作になるのか」という視点で掘り下げることが大切だと思いました。