for (var i = 0; i < 10; i++) { setTimeout(function () { console.log(i); }, 0);}
Ответ
Выведет 10 десять раз. var имеет функциональную область видимости — переменная i одна на весь цикл. Колбэки setTimeout выполняются после завершения цикла, когда i уже равно 10.
Решение
Оптимальное решение (let — блочная область)
for (let i = 0; i < 10; i++) { setTimeout(function () { console.log(i); }, 0);}
let создаёт новую привязку i на каждой итерации — каждый колбэк захватывает своё значение.
Вариант через IIFE
for (var i = 0; i < 10; i++) { (function (i) { setTimeout(function () { console.log(i); }, 0); })(i);}
Вариант через bind
for (var i = 0; i < 10; i++) { setTimeout(console.log.bind(console, i), 0);}
Вариант через параметры setTimeout
for (var i = 0; i < 10; i++) { setTimeout(console.log, 0, i);}
Вариант через with (устаревший, запрещён в strict mode)
for (var i = 0; i < 10; i++) with ({ i: i }) { setTimeout(function () { console.log(i); }, 0); }
with создаёт новую область видимости на каждой итерации, но оператор запрещён в strict mode и снижает производительность. Использовать не стоит.
Вариант со строкой в setTimeout (антипаттерн)
for (var i = 0; i < 10; i++) { setTimeout("console.log(" + i + ")", 0);}
Строка в setTimeout вычисляется как eval — небезопасно, не оптимизируется и ломается при сложных данных. Так делать нельзя.
Вариант через await (меняет поведение — последовательно)
for (var i = 0; i < 10; i++) { await new Promise((resolve) => { setTimeout(() => { console.log(i); resolve(); }, 0); });}
await делает каждую итерацию новой микрозадачей, i печатается до инкремента. Но это уже не тот же код: вывод становится последовательным (с паузами), а не параллельным. Требует async-контекста.