Preventing infinite recursion when using Backbone-style prototypal inheritance

吃可爱长大的小学妹 提交于 2019-12-05 07:04:32

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.

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?

My best solution so far, which feels dreadfully hacky and unscaleable, is to name the method's function in order to be able to reference it directly and compare it with the supposed "parent" method - maybe the first time I've found a use for giving methods a separate function name. Any comments or improvements welcome;

Graph = function () {};
Graph.extend = myExtendFunction;
Graph.prototype = {
   generateScale: function GS() {
       //do stuff
   }
}
 // base class defined elsewhere
UsageGraph = Graph.extend({
   generateScale: function GS() {
       var parentMethod = this.constructor._super.generateScale;
       if(parentMethod === GS) {
           parentMethod = this.constructor._super.constructor._super.generateScale;
       }
       parentMethod.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
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!