59

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

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

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

Qwertiy
  • 123,725
Peter Olson
  • 10,462

2 Answers2

80

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

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

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

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

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

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

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

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

function pass(i) {
  return function () {
    console.log(i);
  }
}

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

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

(function f(i) {
  if (i > 5) return;
  setTimeout(function() {
    console.log(i);
    f(i + 1);
  }, 1000);
})(1);
  1. Использовать Function.prototype.bind(), чтобы создать новую функцию на каждой итерации. Это короче первого варианта, но IE8 и ниже не поддерживают .bind.

for (var i = 1; i <= 5; i++) {
  setTimeout(function(i) {
    console.log(i);
  }.bind(null, i), i * 1000);
}
  1. Передавать аргументы через setTimeout. Это поддерживают все современные браузеры, но если речь идёт о старых, то стоит проверить.

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

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

function doSmth(i) {
  console.log(i);
}

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

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

    Внимание: некоторые браузеры (например, IE 11) поддерживают let, но не поддерживают его в цикле for.

for (let i = 1; i <= 5; i++) {
  setTimeout(function() {
    console.log(i);
  }, i * 1000);
}
Regent
  • 19,134
Peter Olson
  • 10,462
  • Можно ещё (как вариант решения проблемы) использовать setInterval. – Regent Jul 06 '15 at 07:04
  • @Regent Не понял, что имеете ввиду вызов функции с i + 1 в setTimeout. Могли бы объяснить? И да, в этом случае вариант setInterval удобен, но я хотел ответ, который работает в общем случае. Может быть, вместо setTimeout, там запрос AJAX или другая асинхронная функция. – Peter Olson Jul 06 '15 at 07:19
  • Это потому что я не смог внятно объяснить. В общем, я про такой вариант и про то, что он позволяет менять время задержки или досрочно прерывать "цикл" в случае каких-то изменений. При этом он не так чтобы сильно сложнее/длиннее. – Regent Jul 06 '15 at 07:26
  • Так предложенные вами варианты тоже не будут ждать окончания Ajax-запроса. Поэтому вариант с setInterval будет работать ровно так же. – Regent Jul 06 '15 at 07:29
  • @PeterOlson, надеюсь, против правки возражений нет? – Qwertiy Nov 14 '16 at 11:07
  • @Qwertiy спустя 2 года самое время для возражений... "Создавать много одинаковых функций - не очень хорошо, можно поднять эту функцию выше:" - так ведь и в вашем варианте создаётся много одинаковых функций? Хотя код выглядит чище, да – Regent Oct 18 '18 at 09:07
  • @Regent, вложенных функция создаётся n в обоих случаях, а вот внешних - в первом случае тоже n, а в модифицированном - только одна. Пожалуй, действительно стоит на это как-то указать в ответе, только не пойму, как... – Qwertiy Oct 18 '18 at 09:18
  • @Qwertiy понял, 2 * n функций vs n + 1.В ответе можно написать как-нибудь в стиле "Создавать 2*n (n внутренних и n внешних) функций для setTimeout - не очень хорошо, можно вынести создание внутренней функции за цикл, сократив тем самым количество функций до n+1" – Regent Oct 18 '18 at 09:41
  • @Regent эта iв цикле и i в function(i) одинаковая или можно переименовать ? – Никита Фаст Oct 19 '18 at 15:35
  • @НикитаФаст это разные i. "можно переименовать?" - я бы даже сказал "нужно", чтобы не путаться – Regent Oct 19 '18 at 16:33
  • Не могли бы вы переставить 5й вариант в начало? Кажется, уже пора :-) – Pavel Mayorov Apr 17 '22 at 09:00
8

Решение на async

const intArray = [1, 2, 3, 4, 5];

async.eachSeries(intArray, function(i, callback) { setTimeout(function() { console.log(i); callback(); }, i * 1000); }, function(err) { // if any of the file processing produced an error, err would equal that error if( err ) {
console.log('An error occured. All processing has stopped'); } else { console.log('Whole array processed successfully'); } });

<script src="https://cdnjs.cloudflare.com/ajax/libs/async/2.4.0/async.js"></script>
Qwertiy
  • 123,725
Dmitry Taipov
  • 164
  • 1
  • 5