setTimeout のコールバック関数内でローカル変数を使用する

var fruits = ["apple", "orange", "banana"];

という配列があるとき、

for (var i = 0; i < fruits.length; i++) {
    window.setTimeout(function() { alert(fruits[i]); }, i * 1000);
}

こうすると1秒おきに「undefined」が3回表示されてしまう。コールバック関数が呼び出されたときにはすでにローカル変数 i は破棄されている i の値が3になっているためである。
以下のようにコールバック関数を文字列にしておけば、1秒おきに「apple」「orange」「banana」が表示される。

for (var i = 0; i < fruits.length; i++) {
    window.setTimeout("alert('" + fruits[i] + "');", i * 1000);
}

あるいは、以下のように setTimeout の第3引数でコールバック関数へ引数を渡す方法もある。コールバック関数の内容が複雑になる場合はこの方が良い。 by Piroさん

for (var i = 0; i < fruits.length; i++) {
    window.setTimeout(function(aArg) { alert(aArg); }, i * 1000, fruits[i]);
}

Firefox 2以降、JavaScript 1.7以降限定 by nanto_viさん

for (var i = 0; i < fruits.length; i++) {
    window.setTimeout(let (fruit = fruits[i]) function() { alert(fruit); }, i * 1000);
}

こちらは Firefox 1.0 でもOK by nanto_viさん

for (var i = 0; i < fruits.length; i++) {
    with ({ fruit: fruits[i] }) {
        window.setTimeout(function() { alert(fruit); }, i * 1000);
    }
}

クロージャを使って以下のように書く手もある。 by os0xさん

for (var i = 0; i < fruits.length; i++) {
    (function(fruit){
        window.setTimeout(function() { alert(fruit); }, i * 1000);
    })(fruits[i]);
}

TOP

7 Comments to “setTimeout のコールバック関数内でローカル変数を使用する”

コールバック関数の部分が複雑になる場合は、こっちの方がお勧めかも……

for (var i = 0; i < fruits.length; i++) {
    window.setTimeout(function(aArg) { alert(aArg); }, i * 1000, fruits[i]);
}

そういう手もありましたか。サンクスです。本文へ加筆しました。

Firefox 2 以降で JavaScript のバージョンを 1.7 以上に指定すれば、

for (var i = 0; i < fruits.length; i++) {
  window.setTimeout(let (fruit = fruits[i]) function() { alert(fruit); }, i * 1000);
}

でもいけますね。(Firefox 3 ならバージョンを明示的に指定しなくても使えたかも)

あるいは、

for (var i = 0; i < fruits.length; i++) {
  with ({ fruit: fruits[i] }) {
    window.setTimeout(function() { alert(fruit); }, i * 1000);
  }
}

とすれば Firefox 1.0 でも OK ですし。

いずれにせよ、「コールバック関数が呼び出されたときにはすでにローカル変数 i は破棄されている」というのは間違いで、i の値が 4 になっているから fruits[i] が undefined になっているだけです。

便乗させて頂いて、自分ならこう書くかなと。

var fruits = ["apple", "orange", "banana"];
for (var i = 0; i < fruits.length; i++) {
  (function(fruit){
    window.setTimeout(function() { alert(fruit); }, 1000);
  })(fruits[i]);
}

一つ一つ終わってから処理するときはこうしてます。

function setFruitsAlert(fruits) {
   i = 0;
   function g() {
      if(i < fruits.length) {
         window.setTimeout(function(fruits) {
            alert(fruits[i]);
            i++;
            g();
         }, 1000);
      }
   }
   g();
}

var fruits = ["apple", "orange", "banana"];

setFruitsAlert(fruits);

nanto_viさんご指摘ありがとうございます。修正しました。

> 一つ一つ終わってから処理する
あまり自信ないですけど、JS1.7でGenerator使うとこんな感じでしょうか。

function generator() {
    var fruits = ["apple", "orange", "banana"];
    for (var i = 0; i < fruits.length; i++) {
        alert(fruits[i]);
        yield true;
    }
    yield false;
}
function driveGenerator() {
    if (gen.next())
        window.setTimeout(driveGenerator, 1000);
    else
        gen.close();
}
var gen = generator();
driveGenerator();

ジェネレータを使った別解としてこんなのも考えられますね。基本的な発想はhot_coffeeさんと同じです。

function arrayValues(array) {
  for (var i = 0; i < array.length; i++)
    yield array[i];
}
function forEachLater(array, callback, interval) {
  var values = arrayValues(array);
  setTimeout(function () {
    try {
      callback(values.next());
      setTimeout(arguments.callee, interval);
    } catch (ex if ex instanceof StopIteration) {}
  }, interval);
}
forEachLater(["apple", "orange", "banana"], print, 1000);

TOP

TOP