**現象:**
以下のコードの出力結果が予想と異なり、3秒ごとに1~5が順番に出力されない。代わりに3秒後に6が5回表示される。
for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i)
}, 3000);
}
JavaScriptの実行メカニズムとスコープの理解が必要:
JavaScriptは単線程環境であり、コードは上から下へ順番に実行される。ただし、イベントループ機構により非同期処理が可能となる。
タスクは同期タスク(メインスレッドで実行)と非同期タスク(タスクキューに積まれる)に分類される。非同期タスクはメインスレッドが完了した後、キューから取り出されて実行される。
このコードではforループが同期処理で、setTimeoutが非同期処理である。ループ終了時点で変数iは6に設定されているため、すべてのsetTimeoutが実行された際にはiの値が6となり、結果として5回の6が出力される。
**解決策一:クロージャーによる即時実行関数**
for (var i = 1; i <= 5; i++) {
setTimeout(function timer(i) {
console.log(i)
}(i), 3000);
}
この方法では即時実行関数が呼び出されるため、タイムアウトが発生する前にiの値を固定するが、時間差がない。
**最適化:IIFEによるスコープ制御**
for (var i = 1; i <= 5; i++) {
(function (j) {
setTimeout(function timer() {
console.log(j)
}, 3000);
})(i)
}
この構造ではjが各イテレーションで独立して保持されるため、3秒間隔で出力される。
**進化版:変数の遅延実行**
for (var i = 1; i <= 5; i++) {
(function (j) {
setTimeout(function timer() {
console.log(j)
}, j * 1000);
})(i)
}
タイムアウト時間をjの値に合わせることで、1秒ごとの出力が実現される。
**解決策二:関数分割アプローチ**
function timer(i) {
setTimeout(() => {
console.log(i)
}, i *1000);
}
for (var i = 1; i <= 5; i++){
timer(i)
}
関数を別途定義することでスコープ管理が容易になる。
**解決策三:let宣言の利用**
for (let i = 1; i <= 5; i++){
setTimeout(function timer() {
console.log(i)
}, i * 1000);
}
let宣言によりブロックスコープが確保されるため、正しい値が保持される。
**解決策四:Promiseによる待機処理**
function wait(ms) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve()
}, ms);
})
}
for (let i = 1; i <= 5; i++) {
await wait(1000)
console.log(i)
}
非同期処理を明示的に管理し、コード構造を整理できる。