Почему setTimeout в цикле for печатает одно и то же число в JavaScript?
Запускаю в цикле for (var i = 0; i < 3; i++) setTimeout(() => console.log(i), 100) — ожидаю 0,1,2, а печатается 3,3,3. Почему setTimeout в цикле выводит одно число и как получить нормальную последовательность?
2 ответа
Это знаменитая ловушка с var. Дело в двух вещах: var имеет одну общую переменную на весь цикл (нет блочной области видимости), а setTimeout асинхронный — колбэки выполнятся уже после того, как цикл завершился.
К моменту срабатывания таймеров цикл давно закончился, и i равно 3. Все три колбэка смотрят на одну и ту же переменную i, поэтому печатают 3, 3, 3.
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// 3, 3, 3
Решение 1 — заменить var на let (самое простое и правильное). let создаёт новую переменную на каждой итерации, и каждый колбэк запоминает своё значение:
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// 0, 1, 2
Решение 2 — передать i как аргумент setTimeout:
for (var i = 0; i < 3; i++) {
setTimeout((j) => console.log(j), 100, i);
}
// 0, 1, 2
Решение 3 — IIFE (замыкание со своей копией), старый трюк до появления let:
for (var i = 0; i < 3; i++) {
(function (j) {
setTimeout(() => console.log(j), 100);
})(i);
}
Вывод: почти всегда используй let/const вместо var — эта проблема исчезает сама. Это одна из главных причин, почему var считают устаревшим.
Суть в одном предложении: var — одна переменная на всех, let — отдельная на каждую итерацию.
То же самое всплывает не только с setTimeout, но и с любыми отложенными колбэками — например, обработчиками addEventListener, навешанными в цикле. Везде лечится переходом на let. Если видишь в цикле var рядом с асинхронным колбэком — почти наверняка там притаился этот баг.