Разберите следующий JavaScript‑кейс: for (var i = 0; i console.log(i), 0); } Почему он печатает 3,3,3 и какие варианты исправления (без изменения логики цикла) вы предложите, объяснив механизм каждого решения?
Коротко — потому что переменная iii объявлена через `var` (функциональная/глобальная область видимости), колбэки захватывают не текущее значение, а ссылку на одну и ту же переменную iii. Таймауты выполняются после окончания цикла, когда iii стало 333, поэтому все три вывода — 3,3,33,3,33,3,3. Пример временной шкалы: - шаги цикла: i=0,1,2i=0,1,2i=0,1,2 — для каждого создаётся отложенный колбэк; - цикл завершён, iii стало 333; - выполняются отложенные функции — каждая читает текущее iii (= 333). Варианты исправления (не меняя «логики цикла» — т.е. не трогая порядок итераций): 1) Замкнуть значение через IIFE (немедленно вызываемая функция): for (var i = 0; i < 3; i++) { (function(current){ setTimeout(function(){ console.log(current); }, 0); })(i); } Механизм: IIFE получает параметр `current` — это новая локальная переменная, замыкаемая колбэком, фиксирующая значение на момент вызова. 2) Создать локальную блочную копию через `let` внутри тела: for (var i = 0; i < 3; i++) { let j = i; setTimeout(() => console.log(j), 0); } Механизм: `j` — блочная переменная, для каждой итерации создаётся своя копия, которую захватывает колбэк. 3) Использовать фабрику функций: for (var i = 0; i < 3; i++) { function makeLogger(x){ return () => console.log(x); } setTimeout(makeLogger(i), 0); } Механизм: `makeLogger(i)` возвращает замыкание над фиксированным `x`. 4) Использовать `Function.prototype.bind`: for (var i = 0; i < 3; i++) { setTimeout(console.log.bind(null, i), 0); } Механизм: `bind` создаёт новую функцию с уже привязанным аргументом (значение берётся при вызове `bind`). 5) Передать аргумент в `setTimeout` (поддерживается в среде браузера/Node): for (var i = 0; i < 3; i++) { setTimeout(function(x){ console.log(x); }, 0, i); } или for (var i = 0; i < 3; i++) { setTimeout(console.log, 0, i); } Механизм: дополнительные аргументы к `setTimeout` фиксируются при планировании и передаются в колбэк при выполнении. (Доп. простой вариант — заменить `var` на `let` в заголовке цикла: for (let i = 0; i console.log(i), 0); } — это меняет область видимости переменной: для каждой итерации создаётся своё `i`.) Выбор варианта — по стилю и окружению; наиболее идиоматичны сейчас `let` в заголовке цикла или создание блочной копии `let j = i`.
Пример временной шкалы:
- шаги цикла: i=0,1,2i=0,1,2i=0,1,2 — для каждого создаётся отложенный колбэк;
- цикл завершён, iii стало 333;
- выполняются отложенные функции — каждая читает текущее iii (= 333).
Варианты исправления (не меняя «логики цикла» — т.е. не трогая порядок итераций):
1) Замкнуть значение через IIFE (немедленно вызываемая функция):
for (var i = 0; i < 3; i++) {
(function(current){
setTimeout(function(){ console.log(current); }, 0);
})(i);
}
Механизм: IIFE получает параметр `current` — это новая локальная переменная, замыкаемая колбэком, фиксирующая значение на момент вызова.
2) Создать локальную блочную копию через `let` внутри тела:
for (var i = 0; i < 3; i++) {
let j = i;
setTimeout(() => console.log(j), 0);
}
Механизм: `j` — блочная переменная, для каждой итерации создаётся своя копия, которую захватывает колбэк.
3) Использовать фабрику функций:
for (var i = 0; i < 3; i++) {
function makeLogger(x){ return () => console.log(x); }
setTimeout(makeLogger(i), 0);
}
Механизм: `makeLogger(i)` возвращает замыкание над фиксированным `x`.
4) Использовать `Function.prototype.bind`:
for (var i = 0; i < 3; i++) {
setTimeout(console.log.bind(null, i), 0);
}
Механизм: `bind` создаёт новую функцию с уже привязанным аргументом (значение берётся при вызове `bind`).
5) Передать аргумент в `setTimeout` (поддерживается в среде браузера/Node):
for (var i = 0; i < 3; i++) {
setTimeout(function(x){ console.log(x); }, 0, i);
}
или
for (var i = 0; i < 3; i++) {
setTimeout(console.log, 0, i);
}
Механизм: дополнительные аргументы к `setTimeout` фиксируются при планировании и передаются в колбэк при выполнении.
(Доп. простой вариант — заменить `var` на `let` в заголовке цикла:
for (let i = 0; i console.log(i), 0); }
— это меняет область видимости переменной: для каждой итерации создаётся своё `i`.)
Выбор варианта — по стилю и окружению; наиболее идиоматичны сейчас `let` в заголовке цикла или создание блочной копии `let j = i`.