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.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: