Замыкание в цикле: распространённая ошибка
Классическая ошибка: при создании функций внутри цикла с
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создаёт отдельную переменную на итерацию.