• Defining functions in a loop

    Oct 19 2006

    Do you ever try to define a function in a loop, like an event handler for example, and find that it's always the last variable in the array that is pointed at by every event handler? It can take a while to debug this stuff. Let's say you do something like this:

    var links = document.getElementsByTagName('a');
    for (var i=0;i < links.length;i++) {
        var link = links.item(i);
        link.onclick = function() {
            alert(link.href);
            return false;
        };   
    }

    (Stupid example, I know). Well if you run this on a page, you would expect that everytime you click a link, you will get an alert with the href of that link. Well that's wrong! You actually get the href of the last link on the page every time!

    This happens because of closures in JavaScript. You would expect that the link variable has the same value inside the onclick function as it does outside. But in fact, every time the loop comes around, link gets a new value, and actually the link variable in all those other onclick functions changes. So when the loop finishes, every onclick function will alert the href of the last link.

    So to avoid this, you can do a few things. You can make a separate function that attaches the event like so:

    function attachLinkEvent(link) {
        link.onclick = function() {
            alert(link.href);
            return false;
        };   
    }
    
    var links = document.getElementsByTagName('a');
    for (var i=0;i < links.length;i++) {
        var link = links.item(i);
        attachLinkEvent(link);
    }

    This works because inside the attachLinkEvent function, the value of link points at the argument, which is a copy of the link in the loop. This way it doesn't change when the original link changes. Confusing? You bet.

    Rather than use this workaround, you can just avoid using the link variable altogether in the onclick function, and replace it with this. Inside an event handler like onclick, this will point at the element that the event handler is attached to. In this case, this points at the link:

    var links = document.getElementsByTagName('a');
    for (var i=0;i < links.length;i++) {
        var link = links.item(i);
        link.onclick = function() {
            alert(this.href);
            return false;
        };   
    }

    Of course, real life examples are a bit more complicated. There are also different workarounds to fix the problem. But typically, one of these two solutions will fix things.

    For way more information on closures than you will ever need, read Richard Cornford's essay JavaScript Closures

  • Comments

    1. Mark McDonnell at 11:05am on October 20, 2006

    I found this exact problem last year and it drove me barmy trying to figure out what was going wrong.

    In the end I used eval() to resolve the issue.

    So for example:

    var storeNavigation = document.getElementById('id-Navigation');
    var storeLinks = storeNavigation.getElementsByTagName('a');

    for(var i=0; i<storeLinks.length; i++)
    {

    eval("storeLinks[i].onmouseover = function(){ alert(" + i + "); }")

    }

    2. Jesse Skinner at 11:12am on October 20, 2006

    Yeah, it gets more tricky when you want to access something like the loop iterator that you can't get to with 'this'.

    eval() is a nice solution though.

    3. Thomas Chan at 2:13am on January 5, 2007

    OMG Thanks so much!  I was debugging for hours trying to figure out what was wrong, I didn't know why it kept using the last element in the array!

    Nice fix!!

    Thanks!

    4. Andy at 6:54pm on February 9, 2007

    Thanks for this tip!  Worked great.  Like the first commenter, I too needed access to the loop iterator, but my solution was just to pass it as a second parameter to the attachLinkEvent function.

    5. Andy at 2:40pm on April 11, 2007

    Thanks alot! spent more than 5 hours trying to find out what is wrong  with my loop/onClick function build (always showing last item).
    Finally you tip solved my problem..

    6. ken at 1:46pm on June 5, 2007

    Another solution to this problem is to use a function

    var links = document.getElementsByTagName('a');
    for (var i=0;i < links.length;i++) {
        var link = links.item(i);
        link.onclick = myfunc(i);
    }

    function myfunc(i) {
    return function() {
            alert(i);
            return false;
        }
    }

    7. Frank at 3:22pm on September 6, 2007

    OMG, thank you for making this post... i've been trying to figure out what was wrong for the past day!  Thank you so much

    8. tim at 2:53pm on January 15, 2009

    thanks guys this was very helpful.  I too had this problem and this fix works perfectly. I used Ken's function idea, which was better for my real world problem :)

    9. Simon Harper at 9:01am on May 15, 2009

    Thanks alot, you saved at least some of my hair!!

    10. Gaurav Singh at 10:11am on July 11, 2009

    I know this is an old thread but still i wanna thank Mark McDonnell for his very valuable solution of "eval()"

    11. Ravi at 12:57pm on March 2, 2010

    Thanks for the soln. I am new to jave scripting.

    In my code, I parse ajson object and create a link using anchoring <a href="........" onclick="........" >link</a> in a for loop.

    How can i get know what link they clicked created in the for loop

    12. Ravi at 1:02pm on March 2, 2010

    I guess i got it. thanks a ton

    13. Dan Gadd at 4:45pm on March 9, 2010

    I tried several of the solutions on this page and in the comments. The only that I could get to work was the one posted by Ken in comment #6, but I have no idea why. Pass the Tylenol.

    14. owen at 3:49pm on February 24, 2011

    Could someone please explain why the eval() method above works? As I understand it, the 'incorrect' method doesn't work because the functions set to the onclick property continue to reference the original local variable when they are called (instead of using the value the variable held at the time they were assigned to the object's onclick)

    It looks like eval() is able to 'print' the current value of the local variable to the  onclick assignment. Does this work because eval() takes a string, and that string is 'frozen' with whatever value the variable held at the time embedded within it? Is there a way you could avoid eval() and just make a new string variable which would hold that 'frozen' value, and then use the string as the argument in the onclick function? Or would that not work because the string would still somehow be referencing the original local variable...?

    15. Saina at 7:18am on February 19, 2012

    Hello everybody,
    i need your help. if i click Like-button passing just the last value of tempvariable[i].

    hoe can i solve this problem?

      <HTML>
      <HEAD>
      <base target="_blank" >
      <style>
      body {
      padding: 0px;
      margin: 0px;
      background-color: #ffffff;
      }
      #kopf {
      height: 85px;
      margin: 5px;
      background-image: url("blue.PNG");
      }
      #inhalt {
      position:absolute;
      top: 120px;
      left: 340px;
      width: 320px;
      height: 300px;
      background-color: #ffffff;
      }
      #links {
      position: absolute;
      top: 120px;
      left: 10px;
      width: 300px;
      height: 300px;
      background-color: #ffffff;
      }
      #rechts {
      position: absolute;
      top: 120px;
      right: 10px;
      left: 750px;
        width: 300px;
      height: 300px;
      background-color: #ffffff;
      }
    </style>


    <script language="javascript">
    function loadXMLDoc(rq, difficulty)
        {
          xmlhttp=null;
          if (window.XMLHttpRequest)
          {// code for IE7+, Firefox, Chrome, Opera, Safari
            xmlhttp=new XMLHttpRequest();
          }
          else
          {// code for IE6, IE5
            xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");
            }
          if (xmlhttp!=null)
          {
                var value1= difficulty;
                var value3="";
                var difficultyLevel="";
                // value1=firstLevel.options[firstLevel.selectedIndex].value ;
                //value3=EQF1.options[EQF1.selectedIndex].value
                if (value1 != null) {
              .
              .
              .

            if (xmlhttp.readyState==4 && xmlhttp.status==200)
                {
                xmlDoc=xmlhttp.responseXML;
                var txt="";
                x=xmlDoc.getElementsByTagName("location");
                y=xmlDoc.getElementsByTagName("string");
                var txt1="";
                var txt2="";
                var l=0;
                var Ausgabetext="";
                var k=0;
                var j=0;
                var tempvariable=new Array();
                 
                for (i=0; i<x.length; i++){
                txt= x[i].childNodes[0].nodeValue;


                for(j=k; j<y.length; j++)
                      {

                    if(y[j].parentNode.tagName=='title'){
                      txt1=y[j].childNodes[0].nodeValue;
                        j++;
                      break;
                        }
                        }
                        k=j;
                       
                //the value of Discription

                    for(t=l; t<y.length; t++)
                    {
                    if(y[t].parentNode.tagName=='description'){
                    if(y[t].childNodes[0] !==undefined && y[t].childNodes[0].nodeValue!=="null") {
                    txt2=y[t].childNodes[0].nodeValue;
                    t++;
                    break;
                    }
                    else{
                      txt2="There is no Description!";
                      t++;
                      break;
                    }
                      }
                    }
                    l=t ;
                   
                    temp=txt1.link(txt);

                    tempvariable[i]=temp;


                  Ausgabetext+="<li>"+ tempvariable[i]+"<br />"+txt2 +"<br />"+
                                      "<input type=\"button\" value=\"Like\" onclick=\"setlink1(tempvariable[i]);addRow1('likeTable')\" \>"+
                                      "<input type=\"button\" value=\"Dislike\" onclick=\"setlink2(tempvariable[i]);addRow2('dislikeTable')\"\>"+"<br /><hr><br /></li>";
                        }

                    document.getElementById("myDiv").innerHTML="<h3>"+"Our Recommendations for you:"+"</h3>"+Ausgabetext+"<br />";



                }// end of if
              }// end of function()

      xmlhttp.open("GET",url,true);
      xmlhttp.send();
      }//end of if
    else
      {
      alert("Your browser does not support XMLHTTP.");
      }
    }// end of the function LOADXMLDOC

        function setlink1(temp){
    //do something
        }
        function setlink2(temp)
        {
    //do ..   
    }


        function getgoal(variable1, variable2)
        {
        var rq=variable1;
        var difficulty=variable2;
        //alert(difficulty);
        loadXMLDoc(rq, difficulty);
        }
        function addRow1(tableID){
              }
        function addRow2(tableID){
              }

    </script>
    </HEAD>
    <BODY>
     
      <div id="links">
        <?php
    // somthing
    <input type="button" value="Recommend me!" ...

      ?>
    </div>

    <div id="inhalt" >

    <span id="myDiv"> </span>
    </div>
    <div id="rechts">

    <form name='myform' method='POST'>
      <table id="likeTable" width="260px" border="1">
      your like-List:
              <tr>
                  <td>
                  <input type='text' name='mylike1'/>
                  </td>
              </tr>
        </table><BR><BR><BR><hr>
        <table id="dislikeTable" width="260px" border="1">
        your dislike-List:
              <tr>
                  <td>
                  <input type='text' name='mydislike1'/>
                  </td>
              </tr>
        </table>
    <input type='submit' name='submit' value='Add to Favorite'>

      </form>
    </div>


    </BODY>
    </HTML>

    Commenting is now closed. Come find me on Twitter.