В JavaScript дан код: for (var i = 0; i console.log(i), 100); } — объясните, почему вывод не соответствует ожидаемому 0,1,2, как исправить с минимальными изменениями, и развернуто обсудите семантику var/let/const, замыканий и лексического окружения
Код for (var i = 0; i < 3; i++) { setTimeout(()=>console.log(i), 100); } печатает три раза 3, а не 0,1,2. Почему — и как это исправить.
Короткий ответ (минимальное изменение)
Самый простой и правильный способ — заменить var на let: for (let i = 0; i < 3; i++) { setTimeout(()=>console.log(i), 100); } Это приведёт к выводу 0, 1, 2.
Почему так происходит (подробно) 1) var — область видимости функции, одна привязка
Переменные, объявленные через var, имеют функциональную область видимости (или глобальную, если нет функции). В примере цикл создаёт одну переменную i, она инкрементируется до 3 и остаётся равной 3 после окончания цикла.setTimeout ставит колбэки в очередь и выполнит их позже. Когда колбэк выполняется, он обращается не к значению i на момент установки таймера, а к самой переменной i — и её текущее значение уже 3. Поэтому все колбэки видят 3.
2) Замыкание захватывает привязку (binding), а не «снимок» значения
Функции в JavaScript образуют замыкание: они запоминают ссылку на лексическое окружение (binding), где переменная i находится, а не фиксированное числовое значение в момент создания функции. Поэтому все созданные стрелочные функции видят одно и то же окружение, где i к моменту выполнения равна 3.
3) let — блочная область и «пер-итерационное» связывание
В ES6 для for-циклов объявление с let создаёт новую привязку i для каждой итерации цикла (специальное поведение per-iteration binding). Каждая итерация имеет свою отдельную переменную i, которую замыкания «схватывают». Поэтому колбэки печатают 0,1,2.
Альтернативные исправления (минимальные/полезные варианты) 1) Использовать let (рекомендуется): for (let i = 0; i < 3; i++) { setTimeout(()=>console.log(i), 100); }
2) Сразу захватить текущее значение через IIFE (если нельзя менять var): for (var i = 0; i < 3; i++) { (function(j){ setTimeout(()=>console.log(j), 100); })(i); }
3) Передать аргумент в setTimeout (в браузерах/Node setTimeout поддерживает дополнительные аргументы): for (var i = 0; i < 3; i++) { setTimeout(console.log, 100, i); // минимальное изменение } или for (var i = 0; i < 3; i++) { setTimeout((j)=>console.log(j), 100, i); }
4) Использовать .forEach по массиву: [0,1,2].forEach(i => setTimeout(()=>console.log(i), 100));
Кратко о var / let / const и TDZ
var: функция/глобальная область видимости;поднимается (hoisted), инициализируется значением undefined до выполнения кода;нельзя полагаться на блочные границы (if/for) для локализации var.let: блочная область видимости;объявление тоже поднимается, но находится в «temporal dead zone» (TDZ) до места инициализации — доступ до инициализации вызовет ReferenceError;в for-цикле создаёт новую привязку для каждой итерации (per-iteration binding).const: как let, но переменную нельзя переназначить (обязательная инициализация);подходит для значений, которые не должны изменяться; в for (const i = 0; …) нельзя, потому что i нельзя инкрементировать, но можно использовать const в for-of для пер-итерационного значения.
Лексическое окружение и замыкание (семантика)
Когда выполняется функция, создаётся её лексическое окружение (LexicalEnvironment) — список привязок имён к значениям (или к записям в Environment Record).Функция, созданная внутри другой функции/блока, «носит» с собой ссылку на это лексическое окружение — так реализуется замыкание.Замыкание хранит ссылку на запись переменной (binding), а не на фиксированное значение в момент создания. Поэтому если эта запись потом меняется — все замыкания увидят новое значение.Исключение: объявления с let в for-циклах — спецификация создаёт отдельные записи привязки для каждой итерации, поэтому замыкания видят разные привязки с разными значениями.
Практические рекомендации
Используйте let для переменных цикла по умолчанию, если планируете создавать асинхронные колбэки внутри цикла.Понимайте, что замыкание хранит ссылку на переменную: если нужен снимок значения — явным образом захватите его (через аргумент функции/IIFE/передачу в setTimeout).Избегайте var в новом коде, если нет совместимости со старым окружением.
Если нужно, могу показать пошаговую визуализацию лексического окружения на примере вашего кода или варианты для старых сред (ES5).
Код
for (var i = 0; i < 3; i++) {
setTimeout(()=>console.log(i), 100);
}
печатает три раза 3, а не 0,1,2. Почему — и как это исправить.
Короткий ответ (минимальное изменение)
Самый простой и правильный способ — заменить var на let:for (let i = 0; i < 3; i++) {
setTimeout(()=>console.log(i), 100);
}
Это приведёт к выводу 0, 1, 2.
Почему так происходит (подробно)
Переменные, объявленные через var, имеют функциональную область видимости (или глобальную, если нет функции). В примере цикл создаёт одну переменную i, она инкрементируется до 3 и остаётся равной 3 после окончания цикла.setTimeout ставит колбэки в очередь и выполнит их позже. Когда колбэк выполняется, он обращается не к значению i на момент установки таймера, а к самой переменной i — и её текущее значение уже 3. Поэтому все колбэки видят 3.1) var — область видимости функции, одна привязка
2) Замыкание захватывает привязку (binding), а не «снимок» значения
Функции в JavaScript образуют замыкание: они запоминают ссылку на лексическое окружение (binding), где переменная i находится, а не фиксированное числовое значение в момент создания функции. Поэтому все созданные стрелочные функции видят одно и то же окружение, где i к моменту выполнения равна 3.3) let — блочная область и «пер-итерационное» связывание
В ES6 для for-циклов объявление с let создаёт новую привязку i для каждой итерации цикла (специальное поведение per-iteration binding). Каждая итерация имеет свою отдельную переменную i, которую замыкания «схватывают». Поэтому колбэки печатают 0,1,2.Альтернативные исправления (минимальные/полезные варианты)
1) Использовать let (рекомендуется):
for (let i = 0; i < 3; i++) { setTimeout(()=>console.log(i), 100); }
2) Сразу захватить текущее значение через IIFE (если нельзя менять var):
for (var i = 0; i < 3; i++) {
(function(j){
setTimeout(()=>console.log(j), 100);
})(i);
}
3) Передать аргумент в setTimeout (в браузерах/Node setTimeout поддерживает дополнительные аргументы):
for (var i = 0; i < 3; i++) {
setTimeout(console.log, 100, i); // минимальное изменение
}
или
for (var i = 0; i < 3; i++) {
setTimeout((j)=>console.log(j), 100, i);
}
4) Использовать .forEach по массиву:
[0,1,2].forEach(i => setTimeout(()=>console.log(i), 100));
Кратко о var / let / const и TDZ
var:функция/глобальная область видимости;поднимается (hoisted), инициализируется значением undefined до выполнения кода;нельзя полагаться на блочные границы (if/for) для локализации var.let:
блочная область видимости;объявление тоже поднимается, но находится в «temporal dead zone» (TDZ) до места инициализации — доступ до инициализации вызовет ReferenceError;в for-цикле создаёт новую привязку для каждой итерации (per-iteration binding).const:
как let, но переменную нельзя переназначить (обязательная инициализация);подходит для значений, которые не должны изменяться; в for (const i = 0; …) нельзя, потому что i нельзя инкрементировать, но можно использовать const в for-of для пер-итерационного значения.
Лексическое окружение и замыкание (семантика)
Когда выполняется функция, создаётся её лексическое окружение (LexicalEnvironment) — список привязок имён к значениям (или к записям в Environment Record).Функция, созданная внутри другой функции/блока, «носит» с собой ссылку на это лексическое окружение — так реализуется замыкание.Замыкание хранит ссылку на запись переменной (binding), а не на фиксированное значение в момент создания. Поэтому если эта запись потом меняется — все замыкания увидят новое значение.Исключение: объявления с let в for-циклах — спецификация создаёт отдельные записи привязки для каждой итерации, поэтому замыкания видят разные привязки с разными значениями.Практические рекомендации
Используйте let для переменных цикла по умолчанию, если планируете создавать асинхронные колбэки внутри цикла.Понимайте, что замыкание хранит ссылку на переменную: если нужен снимок значения — явным образом захватите его (через аргумент функции/IIFE/передачу в setTimeout).Избегайте var в новом коде, если нет совместимости со старым окружением.Если нужно, могу показать пошаговую визуализацию лексического окружения на примере вашего кода или варианты для старых сред (ES5).