Friday, October 1, 2010

Gah! I Still Don't Know Closures

‹prev | My Chain | next›

I ended a big refactoring of animate-frames last night with some less than DRY code:
  Frame.prototype.remove = function () {
for (var i=0; i<this.elements.length; i++) {
this.elements[i].remove();
};
};

Frame.prototype.show = function() {
for (var i=0; i<this.elements.length; i++) {
this.elements[i].show();
};
};

Frame.prototype.hide = function() {
for (var i=0; i<this.elements.length; i++) {
this.elements[i].hide();
};
};

Frame.prototype.stop = function() {
for (var i=0; i<this.elements.length; i++) {
this.elements[i].stop();
};
};
Let's see if I can DRY that up a bit.

For each of those methods (remove(), show(), hide(), and stop()), I set the prototype property of the Frame object to that repetitive function:
  var methods = ['remove', 'show', 'hide', 'stop'];
for (var i=0; i<methods.length; i++) {
var method = methods[i];
Frame.prototype[methods[i]] = function () {
console.log(method);
for (var j=0; j<this.elements.length; j++) {
this.elements[j][method]();
};
};

}
The only funky thing in there is accessing the method property on each element. element.stop is the same thing as element['stop']—both of them are functions that stop raphaël.js elements. Putting parentheses on either invokes the function: element.stop() and element['stop']().

At least I think that will work. Sadly, when I load that up in my (fab) game, the players do not move at all. Checking the output in the Javascript console, I see that the method being called is always stop()—even when the show() and hide() methods are being invoked.

What gives? Am I messing things up with the property method call?

Actually, no. I have messed up, but what I have messed up here is my Javascript closures. The anonymous function that is being assigned to the Frame.prototype method is itself a closure:
  var methods = ['remove', 'show', 'hide', 'stop'];
for (var i=0; i<methods.length; i++) {
var method = methods[i];
Frame.prototype[methods[i]] = function () {
console.log(method);
for (var j=0; j<this.elements.length; j++) {
this.elements[j][method]();
};
};

}
But the context that it is enclosing includes that var method. My mistake is that the context, the value of var method changes with each iteration through the list of methods. At the end of the iteration, the method variable is set to 'stop'.

What this means is that every one of the methods that I just defined is a stop() method. I do define Frame.prototype.remove(), Frame.prototype.show(), etc. But, when each is called, they use the method from the enclosed context—which is forever more set to "stop".

So what can I do to get the context to stick? I need to add more context—another function:
  var methods = ['remove', 'show', 'hide', 'stop'];
for (var i=0; i<methods.length; i++) {
Frame.prototype[methods[i]] = function(method) {
return function () {
console.log(method);
for (var j=0; j<this.elements.length; j++) {
this.elements[j][method]();
};
};
}(methods[i]);
}
For each method that I am defining, I set the prototype method to the return value of calling the anonymous function with the method name:
    Frame.prototype[methods[i]] = function(method) {
// ...
}(methods[i]);
Now, when I return the same function as earlier, the method variable is in a new, separate context—one that will be remembered when the methods are called. To state is more plainly—in order to ensure that a value in a Javascript closure is what you think it is, it must be defined inside another, enclosing function.

That was unexpectedly difficult. Still, I learned me some good closure. I think I will stop there and move on to resolving the weird bounce issue in animate-frames tomorrow.


Day #243

7 comments:

  1. Here's a little bit different, using a "add_delegating_iterator" method. It looks less brittle to me with respect to overlooking where brackets start & end.

    Stephan

    var a = new Object();
    a.elements = new Array()

    a.each_element = function(method) {
    for (var j=0; j<this.elements.length;j++) {
    console.debug('Would call ' + method + '(' +this.elements[j] + ')')
    }
    }

    a.elements.push('element 1')
    a.elements.push('element 2')

    console.debug('Simple invocation of each_element()')
    a.each_element('stop')
    a.each_element('start')
    console.debug('Invocation of each_element() within function body')
    a.stop = function() { a.each_element('stop')}
    a.stop()

    // Some other method names in our array:
    methods = ['stop_test','start_test', 'end_test']

    a.add_delegating_iterator = function(source) {
    this[source] = function() {
    this.each_element(source)
    }
    }

    // We can iterate over the methods array and
    // get the delegating methods defined.
    for(i=0; i<methods.length;i++) {
    a.add_delegating_iterator(methods[i])
    }

    // Now this does as expected:
    a.start_test()
    a.stop_test()
    // Output is
    // Would call start_test(element 1)
    // Would call start_test(element 2)
    // Would call stop_test(element 1)
    // Would call stop_test(element 2)

    ReplyDelete
  2. I don't know, I would argue this is poor behavior. The var declaration inside the loop should mean that you are defining a distinct variable reference that happens to be named the same.

    ReplyDelete
  3. @Stephan Agreed. Factory functions can definitely add a touch of clarity. I did something similar for the same methods on the frames collection:

    http://github.com/eee-c/my_fab_game/commit/b8b9c3caf6b42955dbd53c76446043e15d59d4c0

    I don't know that I have a preference between the indirection of a separate function or the curly-brace craziness of doing it inline.

    ReplyDelete
  4. @Antonio "var" only creates a distinct variable inside of a function, not a block. To get the kind of behavior that you (and I) expect, you need to use the "let" keyword from javascript 1.7:

    https://developer.mozilla.org/en/New_in_JavaScript_1.7#let_statement

    ReplyDelete
  5. If you don't have Javascript 1.7, you can define your own version of "let:"

    Let's make closures easy

    ReplyDelete
  6. If you use jslint, it gives a warning when you declare a variable anywhere but at the beginning of the function, which would have helped with this subtle bug

    ReplyDelete