Avatar ·

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

Привет! Я только начал изучать JavaScript и столкнулся с проблемой, которую не могу понять. У меня есть цикл, в котором я создаю обработчики событий для нескольких кнопок. Каждая кнопка должна выводить свой номер при клике, но вместо этого все кнопки выводят одно и то же число — последнее значение счётчика.

// Создаём кнопки динамически
for (var i = 0; i < 5; i++) {
    var btn = document.createElement('button');
    btn.textContent = 'Кнопка ' + i;
    btn.addEventListener('click', function() {
        alert('Вы нажали кнопку номер ' + i);
    });
    document.body.appendChild(btn);
}

Когда я кликаю на любую кнопку, появляется сообщение "Вы нажали кнопку номер 5". Почему так происходит? Я ожидал, что каждая кнопка запомнит своё значение i из той итерации, в которой была создана.

Я слышал что-то про замыкания, но не понимаю, как они работают в цикле. Объясните, пожалуйста, как правильно решить эту проблему. Желательно с примерами кода и пояснениями.

<<>>

Проблема: замыкания и общая область видимости

Ваша проблема — классический пример взаимодействия замыканий и области видимости var. Когда вы используете var i, переменная i имеет функциональную область видимости (или глобальную, если объявлена вне функции). В цикле for эта переменная одна на все итерации. К моменту, когда пользователь кликает по кнопке, цикл уже завершился, и i равна 5 (последнее значение после выхода из цикла). Все обработчики ссылаются на одну и ту же переменную i, поэтому все показывают 5.

Чтобы каждая функция «запомнила» значение i на момент своего создания, нужно создать новую область видимости для каждой итерации. Есть три основных способа это сделать:

Способ 1: IIFE (Immediately Invoked Function Expression) — классический

Создаём функцию, которая сразу выполняется и принимает текущее значение i как аргумент. Внутри этой функции создаётся новое замыкание с правильным значением.

for (var i = 0; i < 5; i++) {
    (function(index) {
        var btn = document.createElement('button');
        btn.textContent = 'Кнопка ' + index;
        btn.addEventListener('click', function() {
            alert('Вы нажали кнопку номер ' + index);
        });
        document.body.appendChild(btn);
    })(i);
}

Как это работает: На каждой итерации создаётся новая функция (function(index) { ... }), которая тут же вызывается с аргументом i. Параметр index получает копию значения i на этой итерации. Обработчик клика теперь замыкается на index, который не меняется в следующих итерациях.

Способ 2: Использование let (современный и предпочтительный)

Ключевое слово let создаёт блочную область видимости. В цикле for с let на каждой итерации создаётся новая переменная i, изолированная от других итераций.

for (let i = 0; i < 5; i++) {
    var btn = document.createElement('button');
    btn.textContent = 'Кнопка ' + i;
    btn.addEventListener('click', function() {
        alert('Вы нажали кнопку номер ' + i);
    });
    document.body.appendChild(btn);
}

Как это работает: Каждая итерация цикла получает собственную копию i. Когда обработчик клика замыкается на i, он «захватывает» именно ту копию, которая была создана на данной итерации. Это самый простой и элегантный способ в современном JavaScript.

Способ 3: Использование bind

Метод bind создаёт новую функцию с привязанным контекстом и аргументами. Можно передать текущее значение i как фиксированный аргумент.

for (var i = 0; i < 5; i++) {
    var btn = document.createElement('button');
    btn.textContent = 'Кнопка ' + i;
    btn.addEventListener('click', function(index) {
        alert('Вы нажали кнопку номер ' + index);
    }.bind(null, i));
    document.body.appendChild(btn);
}

Как это работает: .bind(null, i) создаёт новую функцию, в которой первый аргумент (index) всегда будет равен текущему значению i. Обработчик принимает этот аргумент и использует его.

Что выбрать?

  • Если вы пишете современный код (ES6+) — используйте let. Это просто, читаемо и понятно.
  • Если нужно поддерживать старые браузеры (IE11 и ниже) — используйте IIFE.
  • bind — хороший вариант, когда нужно передать не только значение, но и изменить контекст this.

Главное — запомните: замыкания «запоминают» не значение переменной, а ссылку на неё. Чтобы каждая функция «помнила» своё значение, нужно, чтобы у каждой итерации была своя независимая переменная.

<<>>

Альтернативный подход: использование свойств DOM-элемента

Иногда проблему можно решить без замыканий — сохранив нужное значение прямо в свойстве DOM-элемента. Это может быть полезно, когда логика обработчика сложнее и не требует создания множества вложенных функций.

for (var i = 0; i < 5; i++) {
    var btn = document.createElement('button');
    btn.textContent = 'Кнопка ' + i;
    btn.dataset.index = i;  // Сохраняем значение в data-атрибут
    btn.addEventListener('click', function() {
        var index = this.dataset.index;  // Читаем значение при клике
        alert('Вы нажали кнопку номер ' + index);
    });
    document.body.appendChild(btn);
}

Как это работает: Каждой кнопке присваивается атрибут data-index с текущим значением i. Обработчик клика — это одна и та же функция (не замыкание на переменную цикла). При клике он читает значение из this.dataset.index (где this — это кнопка, на которую кликнули).

Плюсы:

  • Не нужно беспокоиться о замыканиях и областях видимости.
  • Код легко читается и отлаживается.
  • Можно использовать с любым типом объявления переменной (var или let).

Минусы: данные хранятся в DOM, что может быть нежелательно для чувствительных данных. Для простых числовых индексов — идеальное решение.

Этот подход особенно удобен, когда нужно динамически добавлять или удалять элементы — данные всегда «привязаны» к самому элементу.