Javascript infamous Loop issue? [duplicate]
I've got the following code snippet.
function addLinks () {
for (var i=0, link; i<5; i++) {
link = document.createElement("a");
link.innerHTML = "Link " + i;
link.onclick = function () {
alert(i);
};
document.body.appendChild(link);
}
}
The above code is for generating 5 links and binding each link with an alert event to show the current link id. But It doesn't work. When you click the generated links they all say "link 5".
But the following code snippet works as our expectation.
function addLinks () {
for (var i=0, link; i<5; i++) {
link = document.createElement("a");
link.innerHTML = "Link " + i;
link.onclick = function (num) {
return function () {
alert(num);
};
}(i);
document.body.appendChild(link);
}
}
The above 2 snippets are quoted from here. As the author's explanation seems the closure makes the magic.
But how it works and How closure makes it work are all beyond my understanding. Why the first one doesn't work while the second one works? Can anyone give a detailed explanation about the magic?
Quoting myself for an explanation of the first example:
JavaScript's scopes are function-level, not block-level, and creating a closure just means that the enclosing scope gets added to the lexical environment of the enclosed function.
After the loop terminates, the function-level variable i has the value 5, and that's what the inner function 'sees'.
In the second example, for each iteration step the outer function literal will evaluate to a new function object with its own scope and local variable num
, whose value is set to the current value of i
. As num
is never modified, it will stay constant over the lifetime of the closure: The next iteration step doesn't overwrite the old value as the function objects are independant.
Keep in mind that this approach is rather inefficient as two new function objects have to be created for each link. This is unnecessary, as they can easily be shared if you use the DOM node for information storage:
function linkListener() {
alert(this.i);
}
function addLinks () {
for(var i = 0; i < 5; ++i) {
var link = document.createElement('a');
link.appendChild(document.createTextNode('Link ' + i));
link.i = i;
link.onclick = linkListener;
document.body.appendChild(link);
}
}
We have 5 divs on the page, each with an ID ... div1, div2, div3, div4, div5
jQuery can do this ...
for (var i=1; i<=5; i++) {
$("#div" + i).click ( function() { alert ($(this).index()) } )
}
But really addressing the problem (and building this up slowly) ...
STEP 1
for (var i=1; i<=5; i++) {
$("#div" + i).click (
// TODO: Write function to handle click event
)
}
STEP 2
for (var i=1; i<=5; i++) {
$("#div" + i).click (
function(num) {
// A functions variable values are set WHEN THE FUNCTION IS CALLED!
// PLEASE UNDERSTAND THIS AND YOU ARE HOME AND DRY (took me 2 years)!
// Now the click event is expecting a function as a handler so return it
return function() { alert (num) }
}(i) // We call the function here, passing in i
)
}
SIMPLE TO UNDERSTAND ALTERNATIVE
If you can't get your head around that then this should be easier to understand and has the same effect ...
for (var i=1; i<=5; i++) {
function clickHandler(num) {
$("#div" + i).click (
function() { alert (num) }
)
}
clickHandler(i);
}
This should be simple to understand if you remember that a functions variable values are set when the function is called (but this uses the exact same thought process as before)
Basically, in the first example you're binding the i
inside the onclick
handler directly to the i
outside the onclick
handler. So when the i
outside the onclick
handler changes, the i
inside the onclick
handler changes too.
In the second example, instead of binding it to the num
in the onclick
handler, you're passing it into a function, which then binds it to the num
in the onclick
handler. When you pass it into the function, the value of i
is copied, not bound to num
. So when i
changes, num
stays the same. The copy occurs because functions in JavaScript are "closures", meaning that once something is passed into the function, it's "closed" for outside modification.
Others have explained what's going on, here's an alternative solution.
function addLinks () {
for (var i = 0, link; i < 5; i++) {
link = document.createElement("a");
link.innerHTML = "Link " + i;
with ({ n: i }) {
link.onclick = function() {
alert(n);
};
}
document.body.appendChild(link);
}
}
Basically the poor mans let-binding.