Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Preventing infinite recursion when using Backbone-style prototypal inheritance

I'm using an extend function adapted from Backbone (identical apart from a few changes to comply with my employer's naming conventions) to implement prototypal inheritance. After setting up the following structure (much simplified below) I get an infinite loop.

Graph = function () {};
Graph.extend = myExtendFunction;
Graph.prototype = {
   generateScale: function () {
       //do stuff
   }
}
 // base class defined elsewhere
UsageGraph = Graph.extend({
   generateScale: function () {
       this.constructor._super.generateScale.call(this); // run the parent's method
       //do additional stuff
   }
})

ExcessiveUsageGraph = Graph.extend({
   // some methods, not including generateScale, which is inherited directly from Usage Graph
})

var EUG = new ExcessiveUsageGraph();
EUG.generateScale(); // infinite loop

The loop is happening because ExcessiveUsageGraph goes up the prototype chain to UsageGraph to run the method, but this is still set to an instance of ExcessiveUsageGraph so when I use this.constructor._super to run the parent method it also goes one step up the chain to UsageGraph and calls the same method again.

How can I reference parent methods from within a Backbone-style prototype and avoid this kind of loop. I also want to avoid referring to parent classes by name if possible.

edit Here's a fiddle demonstrating that this happens in Backbone

like image 392
wheresrhys Avatar asked Apr 04 '12 09:04

wheresrhys


2 Answers

You're running in to one of the limitations of JavaScript's this and prototypal inheritance, squarely because you're attempting to create a class-like inheritance scheme in a language that doesn't directly support it.

Even with Backbone, you are generally discouraged from using "super" directly because of the limitations that you've outlined, and more.

Fixing the problem

The common solution is to call your prototype object directly, instead of trying to mask it through the use of a "super" reference.


UsageGraph = Graph.extend({
   generateScale: function () {
       Graph.prototype.generateScale.call(this); // run the parent's method
       //do additional stuff
   }
})

In a working JSFiddle: http://jsfiddle.net/derickbailey/vjvHP/4/

The reason this works has to do with "this" in JavaScript. When you call a function, the "this" keyword is set based on how you call the function, not where the function is defined.

In the case of calling the "generateScale" method in this code, it's the dot-notation of invoking the generateScale function that sets the context. In other words, because the code reads prototype.generateScale, the context of the function call (the "this" keyword) is set to the prototype object, which happens to be the prototype of the Graph constructor function.

Since the Graph.prototype is now the context of the call to generateScale, that function will run with the context and behavior that you are expecting.

Why this.constructor.super failed

Conversely, when you made the call to this.constructor._super.generateScale, you allowed JavaScript to skew the context in a manner that you didn't expect because of the this keyword at the start.

It's the 3rd level of your hierarchy that's causing the problem with "this". You're calling EUG.generateScale, which is explicitly setting this to the EUG instance. The prototypal lookup for the generateScale method reaches back to the Graph prototype to call the method, because the method is not found on the EUG instance directly.

But this has already been set to the EUG instance, and JavaScript's prototypal lookup respects this. So, when the UsageGraph prototype generateScale is called, this is set to the EUG instance. Therefore, calling this.constructor.__super__ is going to be evaluated from the EUG instance and is going to find the UsageGraph prototype as the value of __super__, which means you're going to call the same method on the same object, with the same context again. Thus, an infinite loop.

The solution is not to use this in prototypal lookups. Use the named function and prototype directly, as I showed in the solution and JSFiddle.

like image 129
Derick Bailey Avatar answered Sep 30 '22 18:09

Derick Bailey


Others have already talked about the limitations of JavaScript's "this", so I won't repeat that. However, it is technically possible to to define a "_super" that will honor the inheritance chain. Ember.js is an example of a library that does this really well. For example in Ember.js, you can do this:

var Animal = Ember.Object.extend({
    say: function (thing) {
        console.log(thing + ' animal');
    }
});

var Dog = Animal.extend({
    say: function (thing) {
        this._super(thing + ' dog');
    }
});

var YoungDog = Dog.extend({
    say: function (thing) {
        this._super(thing + ' young');
    }
});

var leo = YoungDog.create({
    say: function () {
        this._super('leo');
    }
});

leo.say();

leo.say() will output "leo young dog animal" to the console because this._super points to its parent object's method of the same name. To see how Ember is doing this, you can have a look at the Ember.wrap function in Ember's source code here:

http://cloud.github.com/downloads/emberjs/ember.js/ember-0.9.6.js

Ember.wrap is where they wrap every method of an object so that this._super points to the right place. Perhaps you can borrow this idea from Ember?

like image 40
Johnny Oshika Avatar answered Sep 30 '22 17:09

Johnny Oshika