Замыкание в цикле: распространённая ошибка

Классическая ошибка: при создании функций внутри цикла с var все функции замыкают одну и ту же переменную-счётчик, а не её копию на момент итерации, что приводит к неожиданному поведению.

Зачем нужно

Это один из самых часто встречающихся багов в JavaScript — особенно в старом коде с var. Понимание механизма помогает не только исправить ошибку, но и глубже понять, как работают замыкания и области видимости.

Где используется

  • Добавление обработчиков событий в цикле
  • Асинхронные операции (setTimeout, fetch) в цикле
  • Любая функция, созданная в теле цикла с var

Проблема: var и единая переменная

// Ожидается: 0, 1, 2
// Реальность: 3, 3, 3
for (var i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log(i); // все три функции замыкают ОДНУ переменную i
  }, 100);
}
// К моменту вызова setTimeout цикл уже завершился, i === 3
// То же с обработчиками событий
const buttons = document.querySelectorAll('.btn');
for (var i = 0; i < buttons.length; i++) {
  buttons[i].addEventListener('click', () => {
    console.log(i); // всегда buttons.length — ошибка!
  });
}

Решение 1: let (ES6) — самое простое

// let создаёт новую переменную для каждой итерации
for (let i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log(i); // 0, 1, 2 — корректно
  }, 100);
}

// С обработчиками
for (let i = 0; i < buttons.length; i++) {
  buttons[i].addEventListener('click', () => {
    console.log(i); // корректный индекс
  });
}

Решение 2: IIFE (до ES6)

// Немедленно вызываемая функция создаёт новую область для каждой итерации
for (var i = 0; i < 3; i++) {
  (function(j) { // j — копия i на момент вызова
    setTimeout(() => {
      console.log(j); // 0, 1, 2
    }, 100);
  })(i);
}

Решение 3: bind

function logIndex(i) {
  console.log(i);
}

for (var i = 0; i < 3; i++) {
  setTimeout(logIndex.bind(null, i), 100); // привязываем i
}

Решение 4: forEach (создаёт новую область)

// forEach передаёт значение как аргумент — нет замыкания на var
[0, 1, 2].forEach((i) => {
  setTimeout(() => {
    console.log(i); // 0, 1, 2
  }, 100);
});

Реальный сценарий: кнопки с разными ID

const items = ['Яблоко', 'Груша', 'Слива'];

items.forEach((item, index) => {
  const btn = document.createElement('button');
  btn.textContent = item;
  btn.addEventListener('click', () => {
    console.log(`Выбран: ${item}, индекс: ${index}`);
  });
  document.body.append(btn);
});
// Корректно — forEach создаёт новую область для каждой итерации

Частые ошибки

  • Использование var вместо let в циклах с асинхронными операциями — всегда используйте let в современном коде.
  • Использование var с addEventListener в цикле — добавляет обработчик с захватом одной переменной.
  • Ожидание, что const решит проблемуconst в for заголовке вызовет ошибку переназначения; только let создаёт отдельную переменную на итерацию.

Связанные темы

Ресурсы