Categories
closures javascript

Javascript infamous Loop issue? [duplicate]

230

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?

0

    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);
        }
    }
    

    5

    • 3

      Keep in mind that extending the DOM (cf. link.i = i;) is considered as a bad practice.

      – check_ca

      Feb 16, 2014 at 11:44

    • 2

      @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).

      Mar 15, 2014 at 4:20

    • “they can easily be shared if you use the DOM node for information storage” – very educational, thank you!!

      Apr 3, 2014 at 18:01

    • 1

      @check_ca What would you recommend instead? At least this solution works, unlike the closure-based ones.

      Aug 15, 2017 at 13:54

    • 2

      @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")))

      – check_ca

      Aug 16, 2017 at 14:44

    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)

    5

    • 1

      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.

      Apr 25, 2012 at 15:32

    • 3

      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 time

      – Dima

      Dec 10, 2013 at 12:21

    • 1

      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.

      Aug 26, 2015 at 15:33


    • I was looking for some good explanation of closures for a presentation… your approach is by far the best, kudos.

      Apr 20, 2017 at 7:14

    • 1

      To bad that this doesn’t work anymore, the alert simply won’t show, also nothing in the console.

      Aug 15, 2017 at 13:50

    21

    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.

    1

    • 5

      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!

      – bodine

      Dec 11, 2011 at 21:58