Avatar ·

Как в JavaScript заставить замыкания в цикле запоминать значение переменной из своей итерации?

Привет всем! Я новичок в JavaScript и столкнулся с проблемой, которую совсем не понимаю. Пишу простой код: хочу добавить обработчики клика на несколько кнопок, чтобы каждая выводила свой номер. Вот мой код: ```javascript const buttons = document.querySelectorAll('button'); for (var i = 0; i < buttons.length; i++) { buttons[i].addEventListener('click', function() { console.log('Кнопка номер ' + i); }); } ``` Но когда я кликаю на любую кнопку, в консоль всегда выводится "Кнопка номер 5" (последнее значение i), независимо от того, на какую кнопку нажал. Я читал, что это связано с замыканиями, но не понимаю, как это исправить. Почему так происходит? Как сделать, чтобы каждая кнопка запоминала свой номер? Объясните, пожалуйста, простыми словами с примерами. Заранее спасибо! <<>>

Почему возникает проблема?

Проблема связана с тем, как работают замыкания и область видимости переменной, объявленной через 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. Этот способ хорош тем, что его легко тестировать и переиспользовать логику.

Какой бы способ вы ни выбрали, помните: суть проблемы — в разделении одной переменной между всеми замыканиями. Решение всегда в том, чтобы создать для каждой итерации свою копию значения.