Friday, April 27, 2012

Javascript variable capture for Objective-C developers

I've been doing a lot of work with Javascript in web views lately. I'm a total Javascript beginner, but something just clicked, so I thought I'd share.

In Javascript, you'll often see things like this:
for (var i=0; i<5; ++i) {
  var someString = strings[i];

  button.onclick = (function (str) {
    return function (event) {
      alert(str);
    };
  })(someString);
}
You may be wondering why it couldn't just look like this:
for (var i=0; i<5; ++i) {
  var someString = strings[i];

  button.onclick = function (event) {
      alert(someString);
  };
}

There are two things at play here. The first is that for loops don't create a new lexical scope in Javascript. Only functions create scope. That is, this:
for (var i=0; i<5; ++i) {
  var someString = strings[i];
}
is exactly equivalent to this:
var someString;
for (var i=0; i<5; ++i) {
  someString = strings[i];
}
Note that the same variable is reused in every iteration of the loop. At the end of the loop, `someString` will have the value assigned in the last iteration.

The second thing going on is that when Javascript captures a variable in a function, it is captured by reference, not by copy. If you're familiar with Objective-C blocks, you could imagine that every variable you capture is __block-qualified. If you create a bunch of anonymous functions (or closures, or blocks) inside a loop, any variable they capture will always have the value as of the last iteration of the loop.

In Objective-C, it would look something like this. (Remember, every variable we capture in a block is going to be __block-qualified, to mimic Javascript's behavior.)
id reader = // some object
__block i = 0;
for (i=0; i<5; ++i) {
   reader.onload = ^(NSData *fileData) {
     NSLog(@"i: %@", i); // Always logs '5'
     NSLog(@"the data: %@", fileData);
   };
   reader.readFile(someFile);
}
To solve this, we need to pass things that we want to remain immutable (such as i) as arguments to a function. We can't just add another parameter to our onload block, because that's called by reader, which we'll assume we do not control. So instead, we introduce another block:
id reader = // some object
__block i = 0;
for (i=0; i<5; ++i) {

   reader.onload = (^(int param_i) {
       // Imitating JS here by making all captured variables __block
       __block local_i = param_i; 

       return ^(NSData *fileData){
           NSLog(@"i: %@", local_i);
           NSLog(@"theData: %@", fileData);
       };
   })(i);
   // Note that we immediately invoke the block. This returns
   // another block, which has captured the value of `i` at the
   // time we invoked our outer block. That returned block is 
   // the one passed to 'reader.onload'.

   reader.readFile(someFile);
}
Of course, in Objective-C, you'd never do that. But in Javascript, it's the easiest way to create a non-changing copy of a captured variable.