Javascript infamous Loop issue? [duplicate]
Asked Answered
E

5

232

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?

Elfreda answered 20/9, 2009 at 13:21 Comment(0)
B
108

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);
    }
}
Bridgehead answered 20/9, 2009 at 13:43 Comment(5)
Keep in mind that extending the DOM (cf. link.i = i;) is considered as a bad practice.Supervene
@check_ca, however, the same thing can be done with data- attributes, or something like jQuery's .data(). These generally solve the problems in that article (e.g. data- is reserved for users, so a future standard will never define a data-something attribute).Magill
"they can easily be shared if you use the DOM node for information storage" - very educational, thank you!!Manriquez
@Supervene What would you recommend instead? At least this solution works, unlike the closure-based ones.Thready
@PhilippLudwig I would recommend to replace link.i = i with link.setAttribute("data-link-index",i) and replace alert(this.i) with alert(Number(this.getAttribute("data-link-index")))Supervene
T
80

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)

Taber answered 29/1, 2012 at 16:27 Comment(5)
I notice you need some more rep so +1 for the straight-forward version! Although I think I would personally put the clickHandler function declaration outside of the loop, just for style.Fadil
It's a issue I still cant understand good enought. when you say "values are set WHEN THE FUNCTION IS CALLED" you mean that only when CLICKING on div each value of div is set? it's saves on function scope by reference all the timeHonig
I know Im late to the party but anyway. It is called a closure. An inner function can access values in the outer function even when the outer function has returned. So the outer function is an IIFE, so it stores the num value. When you click, the inner function executes and returns the num.Haimes
I was looking for some good explanation of closures for a presentation... your approach is by far the best, kudos.Pug
To bad that this doesn't work anymore, the alert simply won't show, also nothing in the console.Thready
B
22

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.

Brattishing answered 20/9, 2009 at 13:46 Comment(1)
I've read several answers for this topic trying to wrap my head around why. The last half of your last sentence turned the light on in my head finally,... thank you, thank you, thank you!Fourpenny
C
18

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.

Cohlette answered 26/9, 2009 at 21:38 Comment(2)
mm, ive never seen a solution that used the with statement before, nice ;)Hinkel
Be careful when using 'with' statement. It has got some performance issues. webcloud.se/log/JavaScript-and-the-with-statement p2p.wrox.com/content/articles/… yuiblog.com/blog/2006/04/11/with-statement-considered-harmfulCape
R
6

In the first example, you simply bind this function to the onclick event:

function() {alert(i);};

This means that on the click event js should alert the value of the addlink functions i variable. Its value will be 5 because of the for loop().

In the second example you generate a function to be bound with another function:

function (num) {
  return function () { alert(num); };
}

This means: if called with a value, return me a function that will alert the input value. E.g. calling function(3) will return function() { alert(3) };.

You call this function with the value i at every iteration, thus you create separate onclick functions for each links.

The point is that in the first example your function contained a variable reference, while in the second one with the help of the outer function you substituted the reference with an actual value. This is called a closure roughly because you "enclose" the current value of a variable within your function instead of keeping a reference to it.

Roundup answered 20/9, 2009 at 13:40 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.