Исходный код for (var i = 0; i < 3; i++) { setTimeout(function() { console.log(i); }, 100); }
1) Переменная i объявлена через var — она имеет функциональную (или глобальную) область видимости, не блоковую. То есть внутри цикла используется одна и та же переменная i. 2) setTimeout ставит функцию в очередь на выполнение позже (асинхронно). Когда таймеры срабатывают, цикл уже закончился и значение i стало 3. 3) Колбэки замыкают (closure) саму переменную i, а не её значение на каждой итерации. Поэтому все три колбэка читают текущее (на момент выполнения) значение i = 3.
Как исправить (несколько разных способов). Все эти варианты приведут к выводу 0, 1, 2.
1) Использовать let в заголовке цикла (ES6) for (let i = 0; i < 3; i++) { setTimeout(function() { console.log(i); }, 100); } Пояснение: let создаёт новую привязку i для каждой итерации (блоковая область), замыкание захватывает именно эту привязку.
2) Замыкание через IIFE (самовызывающаяся функция) — работает в старых окружениях for (var i = 0; i < 3; i++) { (function(j) { setTimeout(function() { console.log(j); }, 100); })(i); } Пояснение: в параметре j сохраняется текущее значение i, и колбэк замыкает j — оно не изменится.
3) Фабрика функции (явно создать замыкание) function makeLogger(x) { return function() { console.log(x); }; } for (var i = 0; i < 3; i++) { setTimeout(makeLogger(i), 100); } Пояснение: makeLogger захватывает значение x и возвращает функцию, которая его использует.
4) Передать аргумент в setTimeout (поддерживается в браузерах и Node) for (var i = 0; i < 3; i++) { setTimeout(function(x) { console.log(x); }, 100, i); } Пояснение: setTimeout позволяет передать дополнительные аргументы, которые пойдут в колбэк при вызове — там передаётся текущее значение i.
5) Использовать bind для «захвата» значения for (var i = 0; i < 3; i++) { setTimeout(console.log.bind(null, i), 100); } Пояснение: bind создаёт функцию с уже привязанным аргументом i (текущее значение), поэтому при выполнении будет напечатано это значение.
Любой из этих подходов исправит поведение и выведет 0, 1, 2.
Кратко — почему выводится 3 три раза:
Исходный код
for (var i = 0; i < 3; i++) {
setTimeout(function() { console.log(i); }, 100);
}
1) Переменная i объявлена через var — она имеет функциональную (или глобальную) область видимости, не блоковую. То есть внутри цикла используется одна и та же переменная i.
2) setTimeout ставит функцию в очередь на выполнение позже (асинхронно). Когда таймеры срабатывают, цикл уже закончился и значение i стало 3.
3) Колбэки замыкают (closure) саму переменную i, а не её значение на каждой итерации. Поэтому все три колбэка читают текущее (на момент выполнения) значение i = 3.
Как исправить (несколько разных способов). Все эти варианты приведут к выводу 0, 1, 2.
1) Использовать let в заголовке цикла (ES6)
for (let i = 0; i < 3; i++) {
setTimeout(function() { console.log(i); }, 100);
}
Пояснение: let создаёт новую привязку i для каждой итерации (блоковая область), замыкание захватывает именно эту привязку.
2) Замыкание через IIFE (самовызывающаяся функция) — работает в старых окружениях
for (var i = 0; i < 3; i++) {
(function(j) {
setTimeout(function() { console.log(j); }, 100);
})(i);
}
Пояснение: в параметре j сохраняется текущее значение i, и колбэк замыкает j — оно не изменится.
3) Фабрика функции (явно создать замыкание)
function makeLogger(x) {
return function() { console.log(x); };
}
for (var i = 0; i < 3; i++) {
setTimeout(makeLogger(i), 100);
}
Пояснение: makeLogger захватывает значение x и возвращает функцию, которая его использует.
4) Передать аргумент в setTimeout (поддерживается в браузерах и Node)
for (var i = 0; i < 3; i++) {
setTimeout(function(x) { console.log(x); }, 100, i);
}
Пояснение: setTimeout позволяет передать дополнительные аргументы, которые пойдут в колбэк при вызове — там передаётся текущее значение i.
5) Использовать bind для «захвата» значения
for (var i = 0; i < 3; i++) {
setTimeout(console.log.bind(null, i), 100);
}
Пояснение: bind создаёт функцию с уже привязанным аргументом i (текущее значение), поэтому при выполнении будет напечатано это значение.
Любой из этих подходов исправит поведение и выведет 0, 1, 2.