Результат этого кода — три строки с числом 3 (в консоли будет напечатано по очереди 3, 3, 3).
Почему так происходит (коротко):
var i — функциональная (не блочная) переменная. В цикле создаётся одна переменная i, которую все три функции в setTimeout «замыкают» (они хранят ссылку на одну и ту же переменную).setTimeout с задержкой 100 ms выполняет колбэки уже после завершения цикла. К тому моменту цикл закончился и i стало равно 3. Поэтому каждая из функций при вызове читает текущее значение i = 3.
Пояснение с точки зрения событийного цикла: setTimeout планирует колбэк в очередь событий; выполнение колбэков произойдёт позже, когда стек свободен — а к тому времени переменная i уже изменилась.
Три разных способа исправить (с примерами, плюс преимущества/недостатки):
1) Использовать let (ES6), блочная область видимости
for (let i = 0; i < 3; i++) { setTimeout(function() { console.log(i); }, 100); }
Почему работает: при каждой итерации для i создаётся новая блочная привязка, колбэк «замыкает» своё собственное i. Выведет 0, 1, 2.
Плюсы: лаконично, современно, понятное поведение.
Минусы: требует ES6 (в старых окружениях без транспиляции — несовместимо).
2) Создать замыкание через IIFE (подходит для старого JS)
for (var i = 0; i < 3; i++) { (function(j) { setTimeout(function() { console.log(j); }, 100); })(i); }
Почему работает: IIFE получает текущее значение i в параметр j и создаёт новую область, которую замыкает колбэк. Выведет 0, 1, 2.
Плюсы: работает в старых средах без ES6.
Минусы: более многословно, может выглядеть менее современно.
3) Передать значение в setTimeout (или использовать bind) Вариант A — дополнительные аргументы setTimeout:
for (var i = 0; i < 3; i++) { setTimeout(function(j) { console.log(j); }, 100, i); }
Вариант B — bind:
for (var i = 0; i < 3; i++) { setTimeout(console.log.bind(null, i), 100); }
Почему работает: setTimeout позволяет передать аргументы в колбэк (или bind фиксирует аргумент) — передаётся текущее значение i, которое становится независимым от переменной i. Выведет 0, 1, 2.
Плюсы: коротко, явно фиксирует значение.
Минусы: передача дополнительных аргументов setTimeout исторически поддерживается большинством сред, но может выглядеть необычно; bind может менять контекст/параметры (в некоторых случаях не подходит, если нужен специфичный this).
Короткая заметка: аналогично let сработает и const (в for-of/for цикле const даёт новую привязку на каждую итерацию, но значение не может быть переназначено в теле итерации).
Вывод: в современных проектах самый простой и читаемый способ — использовать let (или const), в старых — IIFE или передачу аргументов/ bind.
Результат этого кода — три строки с числом 3 (в консоли будет напечатано по очереди 3, 3, 3).
Почему так происходит (коротко):
var i — функциональная (не блочная) переменная. В цикле создаётся одна переменная i, которую все три функции в setTimeout «замыкают» (они хранят ссылку на одну и ту же переменную).setTimeout с задержкой 100 ms выполняет колбэки уже после завершения цикла. К тому моменту цикл закончился и i стало равно 3. Поэтому каждая из функций при вызове читает текущее значение i = 3.Пояснение с точки зрения событийного цикла: setTimeout планирует колбэк в очередь событий; выполнение колбэков произойдёт позже, когда стек свободен — а к тому времени переменная i уже изменилась.
Три разных способа исправить (с примерами, плюс преимущества/недостатки):
1) Использовать let (ES6), блочная область видимости
for (let i = 0; i < 3; i++) {setTimeout(function() {
console.log(i);
}, 100);
}
Почему работает: при каждой итерации для i создаётся новая блочная привязка, колбэк «замыкает» своё собственное i. Выведет 0, 1, 2.
Плюсы: лаконично, современно, понятное поведение.
Минусы: требует ES6 (в старых окружениях без транспиляции — несовместимо).
2) Создать замыкание через IIFE (подходит для старого JS)
for (var i = 0; i < 3; i++) {(function(j) {
setTimeout(function() {
console.log(j);
}, 100);
})(i);
}
Почему работает: IIFE получает текущее значение i в параметр j и создаёт новую область, которую замыкает колбэк. Выведет 0, 1, 2.
Плюсы: работает в старых средах без ES6.
Минусы: более многословно, может выглядеть менее современно.
3) Передать значение в setTimeout (или использовать bind)
for (var i = 0; i < 3; i++) {Вариант A — дополнительные аргументы setTimeout:
setTimeout(function(j) {
console.log(j);
}, 100, i);
}
Вариант B — bind:
for (var i = 0; i < 3; i++) {setTimeout(console.log.bind(null, i), 100);
}
Почему работает: setTimeout позволяет передать аргументы в колбэк (или bind фиксирует аргумент) — передаётся текущее значение i, которое становится независимым от переменной i. Выведет 0, 1, 2.
Плюсы: коротко, явно фиксирует значение.
Минусы: передача дополнительных аргументов setTimeout исторически поддерживается большинством сред, но может выглядеть необычно; bind может менять контекст/параметры (в некоторых случаях не подходит, если нужен специфичный this).
Короткая заметка: аналогично let сработает и const (в for-of/for цикле const даёт новую привязку на каждую итерацию, но значение не может быть переназначено в теле итерации).
Вывод: в современных проектах самый простой и читаемый способ — использовать let (или const), в старых — IIFE или передачу аргументов/ bind.