Coding with Jesse

Defining functions in a loop

October 19th, 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


Interested in web development? Subscribe to my newsletter!

Comments

1 . Mark McDonnell at 2006-10-20T15:05:12.000Z

Mark McDonnell

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 2006-10-20T15:12:01.000Z

Jesse Skinner

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 2007-01-05T07:13:45.000Z

Thomas Chan

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 2007-02-09T23:54:01.000Z

Andy

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 2007-04-11T18:40:12.000Z

Andy

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 2007-06-05T17:46:45.000Z

ken

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 2007-09-06T19:22:51.000Z

Frank

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 2009-01-15T19:53:04.000Z

tim

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 2009-05-15T13:01:25.000Z

Simon Harper

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

10 . Gaurav Singh at 2009-07-11T14:11:12.000Z

Gaurav Singh

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

11 . Ravi at 2010-03-02T17:57:13.000Z

Ravi

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 2010-03-02T18:02:21.000Z

Ravi

I guess i got it. thanks a ton

13 . Dan Gadd at 2010-03-09T21:45:18.000Z

Dan Gadd

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 2011-02-24T20:49:27.000Z

owen

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 2012-02-19T12:18:42.000Z

Saina

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>