Как в JavaScript заставить замыкания в цикле запоминать значение переменной из своей итерации?
Почему возникает проблема?
Проблема связана с тем, как работают замыкания и область видимости переменной, объявленной через var. Ключевое слово var имеет функциональную область видимости, а не блочную. Переменная i существует одна на весь цикл, а не создается заново на каждой итерации.
Когда вы создаете функцию-обработчик внутри цикла, она запоминает не значение переменной i в момент создания, а ссылку на саму переменную (замыкание). К моменту, когда пользователь кликает на кнопку, цикл уже давно завершился, и i равна последнему значению (buttons.length, например, 5). Все обработчики ссылаются на одну и ту же переменную, поэтому все показывают одно и то же число.
Решение №1: Использовать let (Современный способ, ES6)
Самый простой и правильный способ — заменить var на let. Ключевое слово let имеет блочную область видимости: на каждой итерации цикла создается новая переменная i, и каждая функция-обработчик замыкается на свою собственную копию.
const buttons = document.querySelectorAll('button');
for (let i = 0; i < buttons.length; i++) {
buttons[i].addEventListener('click', function() {
console.log('Кнопка номер ' + i); // Работает правильно!
});
}
Решение №2: Немедленно вызываемая функция (IIFE) — классический паттерн до ES6
До появления let использовали IIFE (Immediately Invoked Function Expression). Мы создаем новую область видимости на каждой итерации, передавая текущее значение i как аргумент функции. Аргумент копируется в локальную переменную index.
const buttons = document.querySelectorAll('button');
for (var i = 0; i < buttons.length; i++) {
(function(index) {
buttons[index].addEventListener('click', function() {
console.log('Кнопка номер ' + index); // Работает правильно!
});
})(i);
}
Решение №3: Использовать forEach или map
Методы массивов, такие как forEach, автоматически создают новую область видимости для каждой итерации через колбэк-функцию. Это очень удобно и читаемо.
const buttons = document.querySelectorAll('button');
buttons.forEach(function(button, index) {
button.addEventListener('click', function() {
console.log('Кнопка номер ' + index); // Работает правильно!
});
});
Решение №4: Использовать атрибут data-*
Можно сохранить индекс прямо в DOM-элементе и читать его при клике. Это не связано с замыканиями, но решает задачу.
const buttons = document.querySelectorAll('button');
for (var i = 0; i < buttons.length; i++) {
buttons[i].dataset.index = i;
buttons[i].addEventListener('click', function() {
console.log('Кнопка номер ' + this.dataset.index); // Работает!
});
}
Вывод
Главное правило: если вы создаете функции в цикле и используете переменную цикла, всегда используйте let (если поддерживается) или создавайте новую область видимости через IIFE. Самый современный и лаконичный способ — let или forEach.
Альтернативный взгляд: Использование стрелочных функций с замыканием через параметр по умолчанию
Есть еще один интересный способ, который редко упоминают, но он работает. Можно создать замыкание, передав значение переменной как параметр по умолчанию в стрелочную функцию. Это сработает, потому что параметр по умолчанию вычисляется в момент создания функции.
const buttons = document.querySelectorAll('button');
for (var i = 0; i < buttons.length; i++) {
buttons[i].addEventListener('click', ((idx = i) => () => {
console.log('Кнопка номер ' + idx);
})());
}
Или более читаемый вариант через отдельную функцию-фабрику:
function createHandler(index) {
return function() {
console.log('Кнопка номер ' + index);
};
}
const buttons = document.querySelectorAll('button');
for (var i = 0; i < buttons.length; i++) {
buttons[i].addEventListener('click', createHandler(i));
}
Этот подход (фабрика функций) очень нагляден: мы явно передаем значение в функцию, которая возвращает новый обработчик. Каждый вызов createHandler(i) создает отдельное замыкание с собственным index. Этот способ хорош тем, что его легко тестировать и переиспользовать логику.
Какой бы способ вы ни выбрали, помните: суть проблемы — в разделении одной переменной между всеми замыканиями. Решение всегда в том, чтобы создать для каждой итерации свою копию значения.