Implementing event listeners (or other asynchronous tasks) within a loop can be challenging. It may seem like you are creating multiple instances of the btnVal
variable for each iteration, but that is not the case. The variable btnVal is hoisted to the top and reused, causing your code to function as follows:
var btnVal;
for (i = 0; i < btn.length; i++) {
btnVal = btn[i].value;
btn[i].addEventListener("click", function() { displayNumber(btnVal) }, false);
}
This means all your event listeners are referencing the same variable, resulting in them only accessing the final value assigned to btnVal when clicked, which would be btn[btn.length -1].value
. Any previous values in the array are disregarded.
There are several approaches to resolve this issue:
1) Instead of relying on a closure variable, extract the value from the element itself when the event occurs.
for (i = 0; i < btn.length; i++) {
btn[i].addEventListener("click", function (event) {
displayNumber(event.target.value);
});
}
2) Move the creation of event listeners into a function and pass btnVal as an argument. Since it becomes a function parameter, it creates a new binding.
for (i = 0; i < btn.length; i++) {
createListener(btn[i], btn[i].value);
}
function createListener(element, val) {
element.addEventListener("click", function () { displayNumber(val) }, false);
}
3) An alternative method is to use an IIFE inline.
for (i = 0; i < btn.length; i++) {
(function (button) {
button.addEventListener("click", function () { displayNumber(button.value) }, false);
})(btn[i]);
}
EDIT: added option 4
4) If ES2015 is available, utilize let
. Let offers block scope, creating a new binding with each loop iteration
for (i = 0; i < btn.length; i++) {
let btnVal = btn[i].value;
btn[i].addEventListener("click", function() { displayNumber(btnVal) }, false);
}