Разберите следующий JavaScript-кейс (вопрос на тонкости семантики): for (var i=0;iconsole.log(i),100); } — объясните вывод программы, почему он такой, и предложите по крайней мере три способа модифицировать код, чтобы он вывёл 0,1,2
Почему: переменная i объявлена через var — она имеет функцию/глобальную область видимости, не блоковую. В теле цикла вы создаёте три таймаута, каждый замыкает одну и ту же переменную i поссылкепо ссылкепоссылке. Таймауты выполнятся асинхронно, после завершения синхронного цикла for, а на тот момент i уже стало равным 3 послепоследнегоi++после последнего i++послепоследнегоi++. Поэтому все три колбэка прочитают текущее значение i = 3.
Как исправить чтобывывести0,1,2чтобы вывести 0, 1, 2чтобывывести0,1,2. Несколько способов:
1) Использовать let в заголовке цикла block−scopedпеременнаяblock-scoped переменнаяblock−scopedпеременная: for (let i = 0; i < 3; i++) { setTimeout(() => console.log(i), 100); }
let создаёт новую привязку i для каждой итерации, замыкания получают разные значения.
2) Закрыть текущее значение в IIFE старыйстильстарый стильстарыйстиль: for (var i = 0; i < 3; i++) { function(j)setTimeout(function()console.log(j);,100);function (j) { setTimeout(function () { console.log(j); }, 100); }function(j)setTimeout(function()console.log(j);,100);iii; }
Здесь в каждую итерацию создаётся локальная j со значением i, и колбэк замыкает j.
3) Передать аргумент в setTimeout аргументыпослезадержкипередаютсявcallbackаргументы после задержки передаются в callbackаргументыпослезадержкипередаютсявcallback: for (var i = 0; i < 3; i++) { setTimeoutfunction(j)console.log(j);,100,ifunction (j) { console.log(j); }, 100, ifunction(j)console.log(j);,100,i; }
Или с arrow-функцией: for (var i = 0; i < 3; i++) { setTimeout((j) => console.log(j), 100, i); }
4) Скопировать значение в локальную let-переменную внутри тела цикла: for (var i = 0; i < 3; i++) { let j = i; setTimeout(() => console.log(j), 100); }
5) Использовать bind, чтобы «захватить» аргумент: for (var i = 0; i < 3; i++) { setTimeoutconsole.log.bind(null,i),100console.log.bind(null, i), 100console.log.bind(null,i),100; }
Все эти варианты приведут к выводу 0, 1, 2 пооднойстрокенакаждыйтаймаутпо одной строке на каждый таймаутпооднойстрокенакаждыйтаймаут. Выбор способа зависит от целевой среды ES6+—простоlet;еслинуженобратныйуровеньсовместимости—IIFEES6+ — просто let; если нужен обратный уровень совместимости — IIFEES6+—простоlet;еслинуженобратныйуровеньсовместимости—IIFE.
Вывод будет
3
3
3
Почему: переменная i объявлена через var — она имеет функцию/глобальную область видимости, не блоковую. В теле цикла вы создаёте три таймаута, каждый замыкает одну и ту же переменную i поссылкепо ссылкепоссылке. Таймауты выполнятся асинхронно, после завершения синхронного цикла for, а на тот момент i уже стало равным 3 послепоследнегоi++после последнего i++послепоследнегоi++. Поэтому все три колбэка прочитают текущее значение i = 3.
Как исправить чтобывывести0,1,2чтобы вывести 0, 1, 2чтобывывести0,1,2. Несколько способов:
1) Использовать let в заголовке цикла block−scopedпеременнаяblock-scoped переменнаяblock−scopedпеременная:
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
let создаёт новую привязку i для каждой итерации, замыкания получают разные значения.
2) Закрыть текущее значение в IIFE старыйстильстарый стильстарыйстиль:
for (var i = 0; i < 3; i++) {
function(j)setTimeout(function()console.log(j);,100);function (j) {
setTimeout(function () { console.log(j); }, 100);
}function(j)setTimeout(function()console.log(j);,100);iii;
}
Здесь в каждую итерацию создаётся локальная j со значением i, и колбэк замыкает j.
3) Передать аргумент в setTimeout аргументыпослезадержкипередаютсявcallbackаргументы после задержки передаются в callbackаргументыпослезадержкипередаютсявcallback:
for (var i = 0; i < 3; i++) {
setTimeoutfunction(j)console.log(j);,100,ifunction (j) { console.log(j); }, 100, ifunction(j)console.log(j);,100,i;
}
Или с arrow-функцией:
for (var i = 0; i < 3; i++) {
setTimeout((j) => console.log(j), 100, i);
}
4) Скопировать значение в локальную let-переменную внутри тела цикла:
for (var i = 0; i < 3; i++) {
let j = i;
setTimeout(() => console.log(j), 100);
}
5) Использовать bind, чтобы «захватить» аргумент:
for (var i = 0; i < 3; i++) {
setTimeoutconsole.log.bind(null,i),100console.log.bind(null, i), 100console.log.bind(null,i),100;
}
Все эти варианты приведут к выводу 0, 1, 2 пооднойстрокенакаждыйтаймаутпо одной строке на каждый таймаутпооднойстрокенакаждыйтаймаут. Выбор способа зависит от целевой среды ES6+—простоlet;еслинуженобратныйуровеньсовместимости—IIFEES6+ — просто let; если нужен обратный уровень совместимости — IIFEES6+—простоlet;еслинуженобратныйуровеньсовместимости—IIFE.