JavaScriptにおけるsetTimeoutとforループの挙動

**現象:**

以下のコードの出力結果が予想と異なり、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)
}

非同期処理を明示的に管理し、コード構造を整理できる。

タグ: javascript setTimeout クロージャー let Promise

9月16日 13:42 投稿