31 March 2010

In Case You Need It

In my last post, I used the following pattern to run some code after it had become available (with typeof thing != 'undefined' replaced here with a fair coin flip):

(function doThingEventually() {
  if (Math.random() > 0.5) {
    alert('hi');
  } else {
    window.setTimeout(doThingEventually, 100);
  }
})();

This does what we want, but as a side effect, it sticks doThingEventually into the global scope. doThingEventually can now be accessed by subsequent bookmarklet calls. That's kinda gross. We might want to know if there's a way to do what we want without any side effects. It turns out that the answer is yes, if we take a page out of the Y combinator’s book:

(function(f, thing, predicate) {
  if (predicate()) {
    thing();
  } else {
    window.setTimeout(function() { f(f, thing, predicate); }, 100);
  }
})(function(f, thing, predicate) {
  if (predicate()) {
    thing();
  } else {
    window.setTimeout(function() { f(f, thing, predicate); }, 100);
  }
}, function() { /* thing(), function to run if predicate() is true */
  alert('hi');
}, function() { /* predicate() that must eventually evalutate to true */
  return Math.random() > 0.5;
});

Running this through the Closure compiler, we get the following:

(function(a,b,c){c()?b():window.setTimeout(function(){a(a,b,c)},100)})(function(a,b,c){c()?b():window.setTimeout(function(){a(a,b,c)},100)},function(){alert("hi")},function(){return Math.random()>0.5});

That code clocks in at 202 bytes. For comparison, here's the compiled version of the original, at 73 bytes:

(function a(){Math.random()>0.5?alert("hi"):window.setTimeout(a,100)})();

We can verify that both methods work: try them yourself! Method one and two. Both of these should, with very high probability, pop up a box saying "hi" to you after a short delay.

Is it worth the 129 extra bytes to not pollute the global namespace? Depends. If you're compiling code that uses that pattern, you should be careful about sticking short identifiers like a into the global namespace, especially in bookmarklet code that needs to be mindful of whatever bizarre code the page author has running. On the other hand, if you have control over all the code that gets run, it's probably saner to just let the compiler keep track of everything and use the simpler variant.

Update: Via a comment by _johnny on Reddit, it turns out Javascript actually has a way of referring to the function being called: arguments.callee. His method doesn't pollute the global namespace, yet maintains conciseness. Unfortunately, according to another comment made in the same thread, arguments.callee is now deprecated. A third way to pull this hat trick off is as follows, courtesy of snorlaxx:

(function() {
  (function doThingEventually() {
    if (Math.random() > 0.5) {
      alert('hi');
    } else {
      window.setTimeout(doThingEventually, 100);
    }
  })()
})()

Phew. Isn't Javascript fun?

Update 1 April 2010: As it turns out, you don't even need the enclosing function to take the variable out of global scope. Apparently just saying (function doThingEventually() { ...etc... })() is fine.

blog comments powered by Disqus