Monday, April 14, 2014

The famous "broken closures in loop" bug

I bet I am not the first who introduced this closure related type of bug in my javascript code:
Say you wanted to add "onclick"-functionaly to a number of similar UI Elements where the behavior for each item should only differ in one parameter - in the following simplified example the element should alert it's index within the list of elements that have the css class "someCssClass" applied:

  var elems = document.getElementsByClassName("someCssClass");  
  for (var i = 0; i < elems.length; i++) {  
     elems[i].addEventListener("click", function() {  
         alert(i);  
     });  
  }  

Expected behavior is that clicking the first UI element alerts "0", the second "1" and so on.

If you tried this out you would see that this does not work as expected. Instead, every element alerts the index of the last element.

Can you spot the bug?


The reason why the index of the last element is being displayed is that the anonymous function is being called after the loop has executed. At this point in time the value of i is already elems.length - 1 for all elements.

To fix this we need to introduce a different "lexical environment". There are multiple ways to do this, here is one which adheres to JSLint's recommodation not to make functions within a loop:


  var elems = document.getElementsByClassName("myClass");  
  for (var i = 0; i < elems.length; i++) {  
   elems[i].addEventListener("click", alertIndex(i));  
  }  
  function alertIndex(i) {  
   return function() {  
    alert(i);  
   };  
  }  

But why is this working?

The reasons are:

1. With the introduction of function "alertIndex" we introduced a new lexical environment. The interpreter works in a way that it is traversing up the lexical environments until it finds the appropriate variable (and "encloses" it as it would be it's own variable - therefore the denomiation "closure"). In that case it does not find it in the anonymous function which is triggering the alert but one level up inside "alertIndex".

2. Variables in outer lexical environments might change, but inner lexical environments do always see the last value.

More about lexical environments and closures can be found at http://javascript.info/tutorial/closures



No comments:

Post a Comment