Tuesday, April 6, 2010

Creating A Javascript Function Inside A Loop

I see the following question asked quite often in #javascript: "I loop over an array of elements and attach an event handler to each one. I pass along an index/variable for such & such reason. The problem is, when the event handler is executed, the index/variable is wrong!"

Example:

for (var i = 0, n = elements.length; i<n; i++) {
  var el = elements[i];
  el.addEventListener('click', function() {
    doSomethingWith(i, el); // i, el are not what you expect!
  }, false);
}

The reason that this is true is somewhat complex, but in basic terms, the function is only actually created once (instead of once each iteration of the loop) and that one function points to the last known values of the variables it uses. For more reading, start with closures and maybe move on to the ECMA Specification, in particular section 13.2, point 1.

The fix to this problem is not too difficult. There is a specific pattern that can be used to ensure that each iteration of the loop creates a brand-new function with the correct values. I call this pattern a "generator function". It probably has a proper name, but I'm not aware of it. The basic idea is to define a function (the generator) which creates and returns functions with the proper variables defined, and then call that generator function for each iteration of the loop, passing in the appropriate values. A typical generator function looks like this:

(function(variable) {
  return function() {
    // do something with variable 
  }
})(value);

There are a few things to notice in this pattern. First is that there are two function expressions: an outer one and an inner one. The outer one is the generator function, and the inner one is the function that contains your original code. Secondly, the generator function expression is wrapped in parenthesis and immediately called with an input of "value". This means that the inner function can use the identifier "variable" and it will refer to whatever value was passed in. The result of this whole shebang is a brand-new function which uses whichever values were passed in to the generator.

Applying this concept to our original problem, we come up with:

for (var i = 0, n = elements.length; i<n; i++) {
  var el = elements[i];
  el.addEventListener('click', (function(i, el) { 
    return function() {
      doSomethingWith(i, el);
    }
  })(i, el), false);
}

It's quite close to the original code, but with a little bit of wrapping. Note that this pattern can be useful for more than just attaching event handlers, although in most cases some form of loop is involved.

14 comments:

  1. hello,
    your explanation

    "but in basic terms, the function is only actually created once (instead of once each iteration of the loop) and that one function points to the last known values of the variables it uses. For more reading, start with closures and maybe move on to the ECMA Specification, in particular section 13.2, point 1."

    is incomplete, it doesn't explain the behavior for implementations that *do not* join objects.

    ReplyDelete
  2. Your "generator function" pattern saved my day. Thanks!

    ReplyDelete
  3. I just arrived to the same conclusion after an evening of thinking... but thanks for the finer solution (I just created manually 3 different functions for the 3 iterations I needed :p )

    ReplyDelete
  4. Many thanks for this trick !

    ReplyDelete
  5. Have been looking for this for years maybe! :D

    ReplyDelete
  6. thank you so much!

    ReplyDelete
  7. THANKS A LOT!!
    I can't believe it took me so long to find this - but it was worthwhile!
    I was struggling a lot, as I'm somewhat novice to javascript, and was writing in coffeescript..
    Btw, the (single-line) expression I came up with to do the same with coffeescript is:
    el.addEventListener('click', ((i,el)->->doSomethingWith(i,el))(i,el) for el,i in elements

    ReplyDelete
  8. You should avoid creating functions in loops. This is still creating a function, but it's one less per iteration.

    function something_wrapper(i, el) {
        return function() {
            doSomethingWith(i, el);
        }
    }
    var i,el; //for loops don't introduce a scope, best not to mislead people into thinking it does.
    for (i = 0, n = elements.length; i.>
        el = elements[i];
        el.addEventListener('click', something_wrapper(i, el), false);
    }

    ReplyDelete