はじめに
JavaScriptでは、varを使ったforループと非同期関数setTimeoutを組み合わせると予期しない動作を引き起こすことがあります。次のコードを見てください:
for (var i = 0; i < 5; i++) {
setTimeout(() => console.log(i), 1000);
}
期待される動作
このコードは1秒の遅延で0から4の数字をログに出力するはずです。
実際の動作
5が5回ログに出力されます。
バグを見つけられますか?どうすれば解決できますか?
バグの理解
このバグは、varが関数スコープを持っているため、すべての反復で同じi変数が共有されることに起因します。ループが終了するまでにsetTimeoutコールバックが実行され、iは5になっています。
解決策: letを使用する
ECMAScript 2015(ES6)で導入されたletはブロックスコープを持ち、各反復ごとに新しいバインディングを作成します:
for (let i = 0; i < 5; i++) {
setTimeout(() => console.log(i), 1000);
}
letがバグを解決する方法:
-
ブロックスコープ:各ループ反復は自身の
iを取得し、各setTimeoutコールバックに正しい値を保持します。
歴史的背景
-
var:JavaScriptの初期から利用可能な関数スコープ。 -
let:ES6(2015)で導入されたブロックスコープ。 -
const:再代入されない変数に使用され、letと同様にブロックスコープを持つ。
違い:
-
var宣言はホイスティングされ、undefinedで初期化されます。 -
letとconst宣言はブロックスコープであり、ステートメントが評価されるまで初期化されません。
詳細については、MDNのvar、let、およびconstのドキュメントを参照してください。
解決策: IIFEとクロージャーを使用する
即時関数呼び出し式(IIFE)は新しい関数スコープを作成し、各反復の現在のi値を保持します。このアプローチはクロージャーを利用して正しい変数値をキャプチャします:
for (var i = 0; i < 5; i++) {
(function(i) {
setTimeout(() => console.log(i), 1000);
})(i);
}
IIFEとクロージャーがバグを解決する方法:
-
関数スコープ:各IIFE呼び出しは新しい関数スコープを作成し、現在の
iをキャプチャします。
歴史的背景
IIFEは早期のJavaScriptバージョンからスコープを作成し、変数のライフタイムを管理するために使用されてきました。クロージャーはJavaScriptの基本的な概念であり、関数が囲むスコープから変数をキャプチャして保持することができます。これらのパターンは、letやconstが利用できないES5やそれ以前のバージョンで特に有用です。
詳細については、MDNのIIFEおよびクロージャーのドキュメントを参照してください。
解決策: console.log.bindを使用する
もう一つの解決策は、現在のi値をキャプチャするバウンド関数を作成するためにconsole.log.bindを使用する方法です:
for (var i = 0; i < 5; i++) {
setTimeout(console.log.bind(console, i), 1000);
}
console.log.bindがバグを解決する方法:
-
バウンド関数:
bindはthisをconsoleに設定し、最初の引数を現在のi値に固定した新しい関数を作成します。これにより、各setTimeoutコールバックが正しいi値を出力します。
歴史的背景
Function.prototype.bindメソッドはECMAScript 5(ES5)で導入されました。初期引数が固定された関数を簡単に作成できるため、スコープを管理し、非同期コールバック内で変数値を保持するための強力なツールです。
詳細については、MDNのbindのドキュメントを参照してください。
結論
変数スコープを理解することは、JavaScriptを習得する上で重要です。setTimeoutを含むよくある面接質問を解決する3つの方法を探りました:
- ブロックスコープを提供する
letを使用する - 正しい変数値をキャプチャするためにIIFEとクロージャーを使用する
- バウンド関数を作成するために
console.log.bindを使用する
これらの解決策を試して、コメントで代替方法を共有してください!