В JavaScript дан код: for (var i = 0; i console.log(i), 100); } — объясните, почему вывод не соответствует ожидаемому 0,1,2, как исправить с минимальными изменениями, и развернуто обсудите семантику var/let/const, замыканий и лексического окружения

26 Сен в 20:30
12 +2
0
Ответы
1

Код
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) Замыкание захватывает привязку bindingbindingbinding, а не «снимок» значения

Функции в JavaScript образуют замыкание: они запоминают ссылку на лексическое окружение bindingbindingbinding, где переменная i находится, а не фиксированное числовое значение в момент создания функции. Поэтому все созданные стрелочные функции видят одно и то же окружение, где i к моменту выполнения равна 3.

3) let — блочная область и «пер-итерационное» связывание

В ES6 для for-циклов объявление с let создаёт новую привязку i для каждой итерации цикла специальноеповедениеper−iterationbindingспециальное поведение per-iteration bindingспециальноеповедениеperiterationbinding. Каждая итерация имеет свою отдельную переменную i, которую замыкания «схватывают». Поэтому колбэки печатают 0,1,2.

Альтернативные исправления минимальные/полезныевариантыминимальные/полезные вариантыминимальные/полезныеварианты 1) Использовать let рекомендуетсярекомендуетсярекомендуется:
for (let i = 0; i < 3; i++) { setTimeout(()=>console.log(i), 100); }

2) Сразу захватить текущее значение через IIFE еслинельзяменятьvarесли нельзя менять varеслинельзяменятьvar:
for (var i = 0; i < 3; i++) {
(function(j){
setTimeout(()=>console.log(j), 100);
})iii;
}

3) Передать аргумент в setTimeout вбраузерах/NodesetTimeoutподдерживаетдополнительныеаргументыв браузерах/Node setTimeout поддерживает дополнительные аргументывбраузерах/NodesetTimeoutподдерживаетдополнительныеаргументы:
for (var i = 0; i < 3; i++) {
setTimeoutconsole.log,100,iconsole.log, 100, iconsole.log,100,i; // минимальное изменение
}
или
for (var i = 0; i < 3; i++) {
setTimeout((j)=>console.log(j), 100, i);
}

4) Использовать .forEach по массиву:
0,1,20,1,20,1,2.forEach(i => setTimeout(()=>console.log(i), 100));

Кратко о var / let / const и TDZ

var:
функция/глобальная область видимости;поднимается hoistedhoistedhoisted, инициализируется значением undefined до выполнения кода;нельзя полагаться на блочные границы if/forif/forif/for для локализации var.let:
блочная область видимости;объявление тоже поднимается, но находится в «temporal dead zone» TDZTDZTDZ до места инициализации — доступ до инициализации вызовет ReferenceError;в for-цикле создаёт новую привязку для каждой итерации per−iterationbindingper-iteration bindingperiterationbinding.const:
как let, но переменную нельзя переназначить обязательнаяинициализацияобязательная инициализацияобязательнаяинициализация;подходит для значений, которые не должны изменяться; в for consti=0;…const i = 0; …consti=0; нельзя, потому что i нельзя инкрементировать, но можно использовать const в for-of для пер-итерационного значения.

Лексическое окружение и замыкание семантикасемантикасемантика

Когда выполняется функция, создаётся её лексическое окружение LexicalEnvironmentLexicalEnvironmentLexicalEnvironment — список привязок имён к значениям иликзаписямвEnvironmentRecordили к записям в Environment RecordиликзаписямвEnvironmentRecord.Функция, созданная внутри другой функции/блока, «носит» с собой ссылку на это лексическое окружение — так реализуется замыкание.Замыкание хранит ссылку на запись переменной bindingbindingbinding, а не на фиксированное значение в момент создания. Поэтому если эта запись потом меняется — все замыкания увидят новое значение.Исключение: объявления с let в for-циклах — спецификация создаёт отдельные записи привязки для каждой итерации, поэтому замыкания видят разные привязки с разными значениями.

Практические рекомендации

Используйте let для переменных цикла по умолчанию, если планируете создавать асинхронные колбэки внутри цикла.Понимайте, что замыкание хранит ссылку на переменную: если нужен снимок значения — явным образом захватите его черезаргументфункции/IIFE/передачувsetTimeoutчерез аргумент функции/IIFE/передачу в setTimeoutчерезаргументфункции/IIFE/передачувsetTimeout.Избегайте var в новом коде, если нет совместимости со старым окружением.

Если нужно, могу показать пошаговую визуализацию лексического окружения на примере вашего кода или варианты для старых сред ES5ES5ES5.

26 Сен в 21:16
Не можешь разобраться в этой теме?
Обратись за помощью к экспертам
Гарантированные бесплатные доработки в течение 1 года
Быстрое выполнение от 2 часов
Проверка работы на плагиат
Поможем написать учебную работу
Прямой эфир