A small trap in jQuery/Javascript closures.

Introduction.

In the course of fixing a broken menu system on one of my pages, when tired, I hit a problem that I chased around for ages. I got the page working again using an awful kludge. Today, in the cool light of dawn I thought about it again with my computer-science head on, worked out what had gone wrong, and wrote a test program to check out my conclusions in my head. When I got up I typed it in, and tested it. The kludge was then quickly removed.

I have reduced the thing to something pretty minimal in the hope that it may save some panic elsewhere.

For the example we will generate, on-the-fly, a page that simply shows a sequence of sister/brother words. When you click on the sister, the brother turns red. The Javascript is the important point, but I will use jQuery because it is convenient and widely understood. To illustrate how I got myself into the trap, we'll assume that the first pair of words are a special case (though in the example they are not.)


function createSpans(critters)
{
   // Get the container div
   var ed = $('#empty');
   // Split the string into words
   var a = critters.split(",");

   // As we've said the first pair are a 'special case', so we have a nested function
   // to deal with that
   function case0(text)
   {
      //create the html for the pair - two spans, the brother in gray
      var s = ' <span>'+text+'</span> ';
      s += '<span style="color:#ddd;">'+text+'</span>';

      // Make the text into a jQuery object
      var item = $(s);
      // Get the spans as jQuery objects
      var sister = item.first();
      var brother = item.next();
      // Fix up the sister
      sister.click(function() { brother.css("color", "red"); });
      return item;
   }

// The rest of them we'll just plough through - same operations
   for (var i = 0; i < a.length; i++)
   {
      var item;
      if (i == 0)    // The special case
      {
         item = case0(a[i]);
         ed.append(item);
         continue;
      }
      // Others are standard

      //create the html for the pair
      var s = '<span>'+a[i]+'</span> ';
      s += '<span style="color:#ddd;">'+a[i]+'</span> ';
      // Make the text into a jQuery object
      item = $(s);
      // Get the spans as jQuery objects
      var sister = item.first();
      var brother = item.next();
      // Fix up the sister
      sister.click(function() { brother.css("color", "red"); });
      ed.append(item);
   }
}

So far so good. We can put this into a page like:
<!doctype html>
<html>
<head>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js" type="text/javascript"/></script>
<script>
var list = "cat,dog,monkey,snake,toad,tortoise";

// Javascript as above here

$(function() {
   createSpans(list);
});
</script>
</head>
<body>
<div id="empty"></div>
</body>
</html>
When we run that we'll get a page like:

When we click on sister word cat (the normal text), the grayed out brother cat next to it turns red as intended. But if we click on dog, it is the grayed out tortoise that turns red - not at all what was intended. So what went wrong?

Well, the relevant parts of the script are where we have:


sister.click(function() { brother.css("color", "red"); });
Our functions are at that point handing out a function object. When that happens, in Javascript and in other languages, a context called a 'closure' is created. The compiler observes that this is happening, and squirrels away a copy of the functions local variables into a safe place, so that later, when someone wants to use the function - as when they click, the required information will be available.

The significant item that's being stored away in this case is the var brother. That is a jScript object? Well no, it is a reference or pointer to a jScript object. What is squirelled away is the pointer.

In the special case function - case0() - this is fine, since we only point brother at the jQuery span object once, so the cat/cat spans work fine.

But in the other cases we assign to the squirelled away pointer every time we go round the loop, and consequently at the end of the loop that pointer points at the tortoise brother span - correct? Well that's what the result shows.

How do we get round this? Well, not late at night when we're tired! We have to force the various brother values to be cached each time round the loop. So another closure is required to do that caching. One small function is enough - each time it is called it will create the required context. The modification is small. At the end of our createSpans() function we make the change:


   var sister = item.first();
   var brother = item.next();
   // Fix up the sister
   //sister.click(function() { brother.css("color", "red"); }); // Removed
   fixup(sister, brother);                                      // Added
   ed.append(item);
The fixup function is trivial, just a thunk if you like. Stick it in after the case0() function:

   function fixup(sister, brother)
   {
      sister.click(function() { brother.css("color", "red"); });
   }
Now, each time the fixup() function is called, a pointer to the vital brother object is cached, and all works as expected.

Hope this saves somebody the time I wasted, which I have to admit was much more than the time to write this. Think first, program later!