LoginSignup
1
0

More than 1 year has passed since last update.

ループ内でコールバック関数に変数を渡すとき、変数のスコープ次第でんぁぁぁぁってなる

Last updated at Posted at 2022-03-31

突然ですが

以下のスクリプトの実行結果は

  (function(){
    let i = 0;
    for (i = 0; i < 3; i++) {
      console.log(i);
      setTimeout(function(){ console.log(`hello ${i}`)}, 1000);
    }  
  })();

このようになります。

0
1
2
hello 3
hello 3
hello 3

Oh……全部3になってやがるぜ……。

変数iのスコープがforのブロック外にあるので、setTimeoutに渡しているコールバック関数は最新のiの値を参照します。
また、letで宣言せずにfor (var i = 0; i < 3; i++)と記述しても同じ結果になります。varは関数以外ではブロックスコープを作りません。

この手のやつ、むかーし(letがないころ)すごいはまりました。イベントリスナーをループ内で設定しようとして「んぁぁぁぁぁぁぁ」ってなってました。ボタン複数作って、全部最後のボタンと同じ挙動になる術式。

変数のスコープを適切にする

シンプルにこのように記述すれば望み通りの結果になります。

(function(){
  for (let i = 0; i < 3; i++) {
    console.log(i);
    setTimeout(function(){ console.log(`hello ${i}`)}, 1000);
  }  
})();

0
1
2
hello 0
hello 1
hello 2

JavaScriptは歴史的な経緯もあり、変数のスコープが少し特殊ですね。

varしか使えないとき

ES6より前であれば以下のようにクロージャを使うといいかもしれません。

(function(){
  for (var i = 0; i < 3; i++) {
    console.log(i);
    var a = function(n) {
      return function() {
        console.log('hello ' + n); // テンプレートリテラルも使えなかった
      }
    };
    setTimeout(a(i), 1000);
  }  
})();

varしか使えないとき②

@esk312 さんにご指摘いただきました。
setTimeoutのコールバックで外部の値を渡すのであれば、第3引数に指定できるそうです!ありがとうございました。

(function(){
  for (var i = 0; i < 3; i++) {
    console.log(i);
    setTimeout(console.log, 1000, 'hello' + i);
  }  
})();

補遺

自分が管理していない日報ツールのブックマークレット作っていたら、敵()のスクリプト内でグローバルスコープの変数をコールバック数に渡していましてね・・・・・・。

1
0
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0