Почему асинхронная функция внутри цикла выполняет последнюю итерацию много раз?

313
26 ноября 2016, 17:53

Есть setTimeout внутри цикла for:

for (var i = 1; i <= 5; i++) { 
    setTimeout(function() { 
        alert(i); 
    }, i * 1000); 
}

Я хочу показывать числа 1, 2, 3, 4, 5, но показывает 6, 6, 6, 6, 6. Почему?

Answer 1

Дело в том, что функция выполняается после того, что цикл закончится. Поэтому, i уже равно 6, когда alert(i) выполняется первый раз.

Если еще непонятно, вот похожий пример в псевдокоде:

У меня 1 камень.
Через минуту скажи, сколько у меня камней.
Дай мне камень сейчас.
Через 2 минуты скажи, сколько у меня камней.
Дай мне камень сейчас.

Получится, что сейчас мне даст 2 камня, в итоге у меня будут 3. Через минуту скажет, сколько у меня камней (т.е. 3), и через две минуты опять скажет, что у меня 3 камня.

Как это решить?

  1. Вставить асинхронную функцию в анонимную функцию, чтобы передать другое значение i функцией alert каждая итерация. Это самое обычное решение.

for(var i = 1; i <= 5; i++) { 
  (function(i) { 
    setTimeout(function() { 
      alert(i); 
    }, i * 1000); 
  })(i); 
}

Создавать много одинаковых функций - не очень хорошо, можно поднять эту функцию выше:

function pass(i) { 
  return function () { 
    alert(i); 
  } 
} 
 
for(var i = 1; i <= 5; i++) { 
  setTimeout(pass(i), i * 1000); 
}

  1. Использовать рекурсию вместо цикла. Это тоже обычное решение.

(function f(i) { 
  if (i > 5) return; 
  setTimeout(function() { 
    alert(i); 
    f(i + 1); 
  }, 1000); 
})(1);

  1. Использовать Function.prototype.bind(), чтобы создать новую функцию каждая итерация. Это короче первого варианта, но IE8 и ниже не поддерживают .bind.

for (var i = 1; i <= 5; i++) { 
  setTimeout(function(i) { 
    alert(i); 
  }.bind(null, i), i * 1000); 
}

  1. Передавать аргументы через setTimeout. Это поддерживают все современные браузеры, но если речь идёт о старых, то стоит проверить.

for(var i = 1; i <= 5; i++) { 
  setTimeout(function (i) { 
    alert(i); 
  }, i * 1000, i); 
}

Замечу, что теперь функции в цикле ничем не отличаются друг от друга, поэтому можно сделать одну функцию:

function doSmth(i) { 
  alert(i); 
} 
 
for(var i = 1; i <= 5; i++) { 
  setTimeout(doSmth, i * 1000, i); 
}

  1. Использовать let (простите, не нашел документацию на русском). Это удобный вариант, но это новая возможность в ECMAScript 2015, так что еще не работает в большинстве браузеров. Если хотите использовать ECMAScript 2015 до того, что браузеры его поддерживают, рекомендую попробовать Babel.

for (let i = 1; i <= 5; i++) { 
  setTimeout(function() { 
    alert(i); 
  }, i * 1000); 
}

READ ALSO
Свернутый текст по умолчанию

Свернутый текст по умолчанию

Добрый деньЕсть код на js, который сворачивает/разворачивает текст статей

295