Замыкание в цикле: классическая задача с var и let

«Что выведет цикл с setTimeout?» — вопрос-ловушка, который задают почти всегда.

Проблема возникает из-за того, что var имеет одну общую переменную на весь цикл, а коллбэки выполняются позже, когда цикл уже закончился.

Ловушка с var

Кажется, что код напечатает 0, 1, 2. На деле — три одинаковых числа.

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log("var:", i), 0);
}

Вывод:

var: 3
var: 3
var: 3

Почему: var i — одна переменная на весь цикл. setTimeout откладывает коллбэки; они срабатывают, когда цикл уже завершился и i стало 3. Все три замыкания смотрят на одну и ту же i.

Решение 1: let создаёт новую переменную на итерацию

let в заголовке цикла создаёт свежую переменную для каждой итерации. Каждый коллбэк замыкается на своё значение.

for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log("let:", i), 0);
}

Вывод:

let: 0
let: 1
let: 2

Решение 2: IIFE

До появления let проблему решали через IIFE (немедленно вызываемое функциональное выражение). Оно создаёт отдельную область видимости и «замораживает» текущее значение в параметре.

for (var i = 0; i < 3; i++) {
  (function (j) {
    setTimeout(() => console.log("iife:", j), 0);
  })(i); // передаём текущее i как j
}

Вывод:

iife: 0
iife: 1
iife: 2

Что такое IIFE

IIFE — функция, которую объявляют и тут же вызывают: (function(){ ... })(). Раньше так создавали изолированную область видимости (модули, приватные переменные), пока не появились блочные let/const и модули ES.

const result = (function () {
  const secret = 42;     // не утекает наружу
  return secret * 2;
})();

console.log(result);

Вывод:

84

Итог

  • С var цикл печатает финальное значение — одна общая переменная на все замыкания.
  • let создаёт новую переменную на каждой итерации — простое решение.
  • IIFE — старый способ изолировать значение и создать отдельную область видимости.
Проверьте себя
1. Что выведет цикл с var i и setTimeout(() => console.log(i), 0) для i от 0 до 2?
A0, 1, 2
B3, 3, 3
C0, 0, 0
Dundefined три раза
2. Почему let решает проблему замыкания в цикле?
Alet работает быстрее
Blet создаёт новую переменную на каждой итерации
Clet делает setTimeout синхронным
Dlet запрещает замыкания
3. Что такое IIFE?
AСпособ объявить класс
BФункция, которую объявляют и сразу вызывают, создавая отдельную область видимости
CВстроенный метод массива
DТип цикла
Поддержать проект