Прив'язка подій в циклі JavaScript

Прив'язка подій в циклі JavaScript

Речь пойдёт о привязке однотипных событий к каждому элементу в массиве. В этой небольшой статье посмотрим на типичную ошибку, причины её возникновения, и, естественно, варианты решения проблемы.

Проблема

Як приклад візьмемо просте завдання: створити 10 кнопок і при натисненні на будь-якій кнопці виводити алерт з її порядоковим номером. Начебто нічого складного. Але якщо не включити голову, то, швидше за все, код буде виглядати якось так:

for (var i = 1; i <= 10; i++) {
	var btn = document.createElement("button")
	btn.innerText = "Button #" + i
	btn.addEventListener('click', function(e) {
		alert(i)
	})
	document.body.appendChild(btn)
}

 

В результаті, на яку б кнопку не клікнули, в алерті будемо бачити одне й те саме число: 11.

Причина

Так чому ж код поводиться подібним чином? Вся справа в тому, що маємо справу з замиканням. А замикання, як відомо, зберігають посилання на своє лексичне оточення, яким в в даному випадку є змінна i. І до того моменту, коли буде натиснута якась із кнопок, цикл вже давно відпрацює, тобто i дорівнюватиме одинадцяти. Тепер те ж саме, але людською мовою. Уявімо, що i - якась коробка (область пам'яті, посилання на яку запам'ятовується), і в ній щось там зберігається (на першій ітерації циклу значення 1). Фактично, коли ми кожній кнопці додаємо подію в циклі, то говоримо: "коли на тебе натискають, он там є коробка з написом i - глянеш, яке в ній значення і виведеш його". Тому і у всіх випадках дорівнюватиме 11.

Вирішення

Цілком очевидно - при додаванні події треба працювати не з посиланням, а з конкретним значенням. Як варіант - скористатися функіональним виразом, що негайно викликається (IIFE - Immediately Invoked Function Expression). Правильно працюючий код буде виглядати так:

for (var i = 1; i <= 10; i++) {
	var btn = document.createElement("button")
	btn.innerText = "Button #" + i
	btn.addEventListener('click', (function(i) {
		return function() {
			alert(i)
		}
	})(i))
	document.body.appendChild(btn)
}

 

Можна зробити й по-іншому - прив'язати нову функцію і явно вказати контекст:

for (var i = 1; i <= 10; i++) {
	var btn = document.createElement("button")
	btn.innerText = "Button #" + i
	btn.addEventListener('click', listener.bind(null, i))
	document.body.appendChild(btn)
}

function listener(index) {
	alert(index)
}

 

А можна і звернутися за допомогою до фабричної функції:

for (var i = 1; i <= 10; i++) {
	var btn = document.createElement("button")
	btn.innerText = "Button #" + i
	btn.addEventListener('click', listener(i))
	document.body.appendChild(btn)
}

function listener(index) {
	return function() {
		alert(index)
	}
}

 

На сьогодні все. Додатково раджу почитати про замикання на MDN