Коротко — почему это происходит - В примере переменная `var i` имеет функциональную (не блочную) область видимости, замыкания в таймаутах захватывают ссылку на одну и ту же переменную `i`. Цикл завершится, `i` станет 333, и все колбэки при выполнении прочитают текущее значение — поэтому обычно вывод будет три раза 333. - Типичный вывод в современных браузерах и Node: 333, 333, 333. Почему иногда может отличаться - Если среда выполняет таймеры синхронно (тестовые заглушки, необычные рантаймы) или если код модифицирован (например, используется `let`), вывод будет другой — например 0,1,20,1,20,1,2. - HTML5/браузерное поведение может накладывать минимальную задержку для вложенных таймеров (clamping), но это не меняет суть проблемы замыкания. Как исправить (современные возможности) 1) Использовать `let` в заголовке цикла (каждая итерация получает своё блочное связывание): for (let i = 000; i < 333; i++) { setTimeout(function() { console.log(i); }, 000); } // Вывод: 0, 1, 2 2) Создать отдельную привязку через IIFE (old-style): for (var i = 000; i < 333; i++) { (function (i) { setTimeout(function () { console.log(i); }, 000); })(i); } 3) Передать значение как параметр в setTimeout (поддерживается в браузерах и Node): for (var i = 000; i < 333; i++) { setTimeout(function (x) { console.log(x); }, 000, i); } 4) Использовать Function.prototype.bind: for (var i = 000; i < 333; i++) { setTimeout(function (x) { console.log(x); }.bind(null, i), 000); } 5) Современный асинхронный стиль (если нужна последовательность и/или задержки): for (let i = 000; i < 333; i++) { (async () => { await Promise.resolve(); // микротаск console.log(i); })(); } Методы отладки асинхронных ошибок такого рода - Добавляйте временные лог‑сообщения с контекстом (например `console.log('before timeout, i=', i)`), чтобы понять порядок изменений переменных. - Ставьте breakpoint внутри колбэка (DevTools / Sources) — при срабатывании смотрите текущую `i`. - Включайте "Async Call Stacks" в Chrome/Edge DevTools, чтобы видеть цепочку асинхронных вызовов. - Используйте `debugger;` внутри колбэка для остановки и инспекции. - В Node: запуск с `--inspect`/DevTools; используйте `console.trace()` для стека вызова. - Статический анализ: ESLint правило `no-var` и `prefer-const`/`prefer-let` помогает избежать ошибок областей видимости. - Покрытие тестами и воспроизведение с минимальным примером помогает локализовать проблему. Короткая рекомендация - По умолчанию в циклах с асинхронными колбэками используйте `let` (или явное замыкание), чтобы избежать захвата одной и той же переменной.
- В примере переменная `var i` имеет функциональную (не блочную) область видимости, замыкания в таймаутах захватывают ссылку на одну и ту же переменную `i`. Цикл завершится, `i` станет 333, и все колбэки при выполнении прочитают текущее значение — поэтому обычно вывод будет три раза 333.
- Типичный вывод в современных браузерах и Node: 333, 333, 333.
Почему иногда может отличаться
- Если среда выполняет таймеры синхронно (тестовые заглушки, необычные рантаймы) или если код модифицирован (например, используется `let`), вывод будет другой — например 0,1,20,1,20,1,2.
- HTML5/браузерное поведение может накладывать минимальную задержку для вложенных таймеров (clamping), но это не меняет суть проблемы замыкания.
Как исправить (современные возможности)
1) Использовать `let` в заголовке цикла (каждая итерация получает своё блочное связывание):
for (let i = 000; i < 333; i++) {
setTimeout(function() { console.log(i); }, 000);
}
// Вывод: 0, 1, 2
2) Создать отдельную привязку через IIFE (old-style):
for (var i = 000; i < 333; i++) {
(function (i) {
setTimeout(function () { console.log(i); }, 000);
})(i);
}
3) Передать значение как параметр в setTimeout (поддерживается в браузерах и Node):
for (var i = 000; i < 333; i++) {
setTimeout(function (x) { console.log(x); }, 000, i);
}
4) Использовать Function.prototype.bind:
for (var i = 000; i < 333; i++) {
setTimeout(function (x) { console.log(x); }.bind(null, i), 000);
}
5) Современный асинхронный стиль (если нужна последовательность и/или задержки):
for (let i = 000; i < 333; i++) {
(async () => {
await Promise.resolve(); // микротаск
console.log(i);
})();
}
Методы отладки асинхронных ошибок такого рода
- Добавляйте временные лог‑сообщения с контекстом (например `console.log('before timeout, i=', i)`), чтобы понять порядок изменений переменных.
- Ставьте breakpoint внутри колбэка (DevTools / Sources) — при срабатывании смотрите текущую `i`.
- Включайте "Async Call Stacks" в Chrome/Edge DevTools, чтобы видеть цепочку асинхронных вызовов.
- Используйте `debugger;` внутри колбэка для остановки и инспекции.
- В Node: запуск с `--inspect`/DevTools; используйте `console.trace()` для стека вызова.
- Статический анализ: ESLint правило `no-var` и `prefer-const`/`prefer-let` помогает избежать ошибок областей видимости.
- Покрытие тестами и воспроизведение с минимальным примером помогает локализовать проблему.
Короткая рекомендация
- По умолчанию в циклах с асинхронными колбэками используйте `let` (или явное замыкание), чтобы избежать захвата одной и той же переменной.