はじめに
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
を使用する
これらの解決策を試して、コメントで代替方法を共有してください!