I will try to explain what happens in the code.
The first point worth noting is the "Declaration Hoisting":
link
Which means raising the variable declarations to the top of the scope, which can be global or function. That is:
for(var i=0; i < nums.length; i++)
It's how you do it:
var i;
for(i = 0; i < nums.length; i++)
In this way the variable i will be available for use by everyone within the scope.
And this is why when you add a click this way:
elem.addEventListener('click', function() {
alert(nums[i])
});
It will always result in the last number being displayed. Because the anonymous function you created has access to variables i and nums that were in the same scope as the anonymous function. Meaning that at the moment the function is executed the variable i contains the index of the last Array element.
IIFE
To mitigate this problem, we use an Immediately Invoked Function Expression (IIFE), a function that executes immediately when it is created. Usually expressed this way:
(function(){
}())
Or
(function(){
})()
Although I personally prefer the first option. By doing what you demonstrate in the code, the following happens:
IFFE runs during the loop and receives as a parameter the value contained in the current Array index;
elem.addEventListener('click', (function(num) {
})(nums[i]));
As a new role has been declared, a new scope is created. This scope has a variable called num that contains the value contained in the index of the array that the function was called with.
You return a new function which, when executed, will call the alert function, passing it as a parameter;
return function() {
alert(num);
};
This new function, which was created in the return clause, is then added as an event listener;
elem.addEventListener('click', (function(num) {
return function() {
alert(num)
};
})(nums[i]));
In your case, clicking always displays the last Array number because, as I understand it, you call addEventListener multiple times in the same element. So, when adding a new listener, the old one is overwritten.