JavaScriptの非同期処理とスコープの関係性について

今回非同期処理スコープの関係性について紹介したいと思います。

唐突ですが以下のコードをご覧になってください。

function countdown() {
  let i;
  for (i = 5; i >= 0; i--) {
    setTimeout(function() {
      console.log(i === 0 ? "GO!" : i);
    }, (5 - i) * 500);
  }
}
countdown();

出力結果はどうなると思いますか?

結果は以下になります。

f:id:top_men:20181103122242p:plain

このコードのポイントとしては、変数iがfor文の外にあるということです。setTimeoutが実行される時にはiの値が-1になっているのでsetTimeoutが参照する値が2行目のiつまり-1になります。

では以下のコードではどうなるのでしょう?

function countdown() {
  for (let i = 5; i >= 0; i--) {
    setTimeout(function() {
      console.log(i === 0 ? "GO!" : i);
    }, (5 - i) * 500);
  }
}
countdown();

結果は以下となります。

f:id:top_men:20181103163708p:plain

こちらは意図としたように動きます。 先ほどのコードとの違いは、変数iをforループの制御文の箇所で利用しています。 JavaScriptの処理系はループの各ステップで新しい独立した変数iのコピーを作成します。setTimeoutに渡された関数が実行されるときに、自分の独自のスコープになる変数から値を受け取ることになります。

言葉ではなかなかわかりづらいので下記のコードをみてみてください。

// 1回目
{
  let i = 5
  setTimeout(function() {
    console.log(i === 0 ? "GO!" : 5); // 5
  }, (5 - 5) * 500);
}

// 2回目
{
  let i = 4
  setTimeout(function() {
    console.log(i === 0 ? "GO!" : 4); // 4
  }, (5 - 4) * 500);
}

// 3回目
{
  let i = 3
  setTimeout(function() {
    console.log(i === 0 ? "GO!" : 3); // 3
  }, (5 - 3) * 500);
}

// 4回目
{
  let i = 2
  setTimeout(function() {
    console.log(i === 0 ? "GO!" : 2); // 2
  }, (5 - 2) * 500);
}

// 5回目
{
  let i = 1
  setTimeout(function() {
    console.log(i === 0 ? "GO!" : 1); // 1
  }, (5 - 1) * 500);
}

// 6回目
{
  let i = 0
  setTimeout(function() {
    console.log(i === 0 ? "GO!" : 0); // "GO!"
  }, (5 - 0) * 500);
}

setTimeout関数が実行される時には上記のように各ステップで新しくコピーされた変数iを参照していることになります。

まとめ

どうしてこのような結果になるのか最初理解するのに時間が掛かりました。JavaScriptにおける非同期処理スコープについてはちゃんと理解しておく必要があります。 間違いがあれがご指摘ください!

参考記事・書籍

js-next.hatenablog.com

www.oreilly.co.jp