The Problem:

Every programmer who's tried to apply classical Object Oriented techniques when developing with JavaScript, has at one time or another asked themselves the question: How do you call or invoke a super class method? Before the Ajax craze got into full swing, this question rarely arose because most developers were only exposed to the language when doing client-side form validation or simple DHTML / DOM element visibility toggling. In those simple situations, functional programming is sufficient and OO is of lesser importance. Now that Ajax is all the rage, programmers have been building increasingly complex systems involving large amounts of client-side JavaScript. As a result, many have tried to apply OO techniques to JavaScript as a way to manage that complexity. In the process, most developers quickly realize that JavaScript is a prototypical language and as a result lacks many of the more familiar OO conventions.

The subject of OO design and it's various topics is huge, but aside from class definition approaches, I think this particular topic is one that JavaScript developers try to solve more frequently than any other. As a result, there are many different examples that you can find on the Internet, but all of the ones I looked at have certain scenarios under which they do not work properly. My interest in this topic grew as a result of my team's efforts in developing the ThinWire Ajax Framework. As the demands on the client-side portion of the framework increased, we reached a point where it became necessary to have a solid OO model that supported super class method calling. Through super class calls, you can further centralize common code in the class hierarchy and as a result, more easily eliminate duplicate code.

The following outlines a number of approaches that I came across in my search. Ultimately, I was unable to find an acceptable solution published anywhere, so I had to resort to a custom solution, which you'll find toward the end of this article. While super class calling was the most important OO mechanism that I needed a working model for, I also wanted a more natural way to define classes using JavaScript since the accepted prototypical approach is somewhat ugly in my opinion. So, with that said, lets dive in. As many developers have discovered, it's very easy to do basic inheritance in JS, in fact there are two widely known approaches.

Simple Inheritance without Super Calls:
//Ahead-Of-TimeJavaScriptClassDefinitionandInheritance
//Yikes,that'skindofugly.
functionBaseClass() {
//BaseClassconstructorcodegoeshere
}

BaseClass.prototype.getName =function() {
return"BaseClass";
}

functionSubClass() {
//SubClassconstructorcodegoeshere
}

//InheritthemethodsofBaseClass
SubClass.prototype =newBaseClass();

//Overridetheparent'sgetNamemethod
SubClass.prototype.getName =function() {
return"SubClass";
}

//Alerts"SubClass"
alert(newSubClass().getName());

And this approach, but you should avoid it:
//Run-TimeJavaScriptClassDefinitionandInheritance
//Looksmoretraditional,butJSenclosurescanleadto
//SEVEREmemoryleaksinInternetExplorer.
functionBaseClass() {
this.getName =function() {
return"BaseClass";
};

//BaseClassconstructorcodegoeshere
}

functionSubClass() {
//Overridetheparent'sgetNamemethodat
//objectinstancecreationtime
this.getName =function() {
return"SubClass";
}

//SubClassconstructorcodegoeshere
}

//InheritthemethodsofBaseClass
SubClass.prototype =newBaseClass();

//Alerts"SubClass"
alert(newSubClass().getName());

Like I mention in the comments the first approach is somewhat unsightly, but it's preferred over the second approach due to memory, performance and other issues. Its only been included here so that I could point out that you should not use it! So focusing on the first example, which outlines the standard prototypical approach, the question is: How can a method of SubClass call a method of its BaseClass? Well, here's one approach that a lot of people try and take.

A Common and Broken Super Calling Attempt:
functionBaseClass() {}
BaseClass.prototype.getName =function() {
return"BaseClass("+this.getId() +")";
}

BaseClass.prototype.getId =function() {
return1;
}

functionSubClass() {}
SubClass.prototype =newBaseClass();
SubClass.prototype.getName =function() {
//CallthegetName()methodofthesuperclass
//Hmm...isanexplicitreferencereallyasupercall?
return"SubClass("+this.getId() +")extends"+
BaseClass.prototype.getName();
}

SubClass.prototype.getId =function() {
return2;
}

//Alerts"SubClass(2)extendsBaseClass(1)";
//Isthistheproperoutput?
alert(newSubClass().getName());

The code above is modified version of the first script, I just removed the comments and some white space so you can focus on the new getId() method and the super call. Although, you have to wonder if that really qualifies as a super call since it has a hard coded reference to BaseClass. Nonetheless, it does the job... or does it? One thing that a proper polymorphic super call must do, is guarantee that the 'this' reference always refers to the current object instance and class methods. With that in mind, look closely at the output that is alerted and then look closely at the getName() method declaration for BaseClass. Do you see the problem? The issue is subtle, but very important. Using the super call syntax above, the getName() method of BaseClass is called and it returns a string containing both the name of the class and the value returned by 'this.getId()'. The problem is that 'this.getId()' should return 2, not 1. If this seems strange to you then you might want to read up on polymorphism in an OO language like Java or C#. In any case, here's a slight modification you can make to solve the problem.

Static ( Hard Coded ) Super Calling That Works:
functionBaseClass() {}
BaseClass.prototype.getName =function() {
return"BaseClass("+this.getId() +")";
}

BaseClass.prototype.getId =function() {
return1;
}

functionSubClass() {}
SubClass.prototype =newBaseClass();
SubClass.prototype.getName =function() {
//Alittle'apply'magicandpolymorphismworks!
//Butugh,explicitreferences!
return"SubClass("+this.getId() +")extends"+
BaseClass.prototype.getName.call(this);
}

SubClass.prototype.getId =function() {
return2;
}

//Alerts"SubClass(2)extendsBaseClass(2)";
//Hey,wegottheproperoutput!
alert(newSubClass().getName());

The call() method is defined as a member method of all Function instances in the ECMA-262 JavaScript/EcmaScript standard and has been supported by all major web browsers for sometime now. You may already know this, but JavaScript treats all functions as though they are objects and therefore they can have methods and properties attached to them. The call() method allows you to call a function and specify what the 'this' variable should be during the invocation of the function. JavaScript functions are not tightly bound to their objects and therefore if you do not use call(), the object directly to the left of the function is always passed as the 'this' variable. Another method that you'll probably see in use that works very similarly to call(), is the apply() method. The only difference between the two is that apply() accepts an array of arguments that it passes to the function, whereas call() accepts the arguments individually.

Now looking back at the last example, the only the problem with it is that the super class references are explicit and therefore hard-coded. This may be acceptable for small class hierarchies, but for larger hierarchies with deeper levels of inheritance, explicit references can be very tedious to maintain. So, what's the solution? Unfortunately, there is no simple solution. Simply put, JavaScript was never built to support super class method calling via implicit references. There is no equivalent 'super' variable like the one you find in other OO languages. As a result, many developers have proposed solutions but as I mentioned earlier, each solution I have seen breaks down in one way or another. For example, here is one of the most famous approaches outlined by the JavaScript guru Douglas Crockford in his article entitled "Classical Inheritance in JavaScript".

Douglas Crockford's Almost Working Approach:

One Time Support Code
//Crockford'sapproachaddsthe'inherits'method
//toallfunctions,aswellasaper-classmethod
//called'uber'thatallowsyoutomakesupercalls.
Function.prototype.inherits =function(parent) {
var d =0, p = (this.prototype =newparent());

this.prototype.uber =function(name) {
var f, r, t = d, v = parent.prototype;
if (t) {
while (t) {
v = v.constructor.prototype;
t -=1;
}
f = v[name];
}else{
f = p[name];
if (f ==this[name]) {
f = v[name];
}
}
d +=1;
r = f.apply(this,Array.prototype.slice.apply(arguments,[1]));
d -=1;
return r;
};
};

Working Example
functionBaseClass() {}
BaseClass.prototype.getName =function() {
return"BaseClass("+this.getId() +")";
}

BaseClass.prototype.getId =function() {
return1;
}

functionSubClass() {}
SubClass.inherits(BaseClass);
SubClass.prototype.getName =function() {
//Looksprettycleananditcalls
//thegetName()methodofBaseClass
return"SubClass("+this.getId() +")extends"+
this.uber("getName");
}

SubClass.prototype.getId =function() {
return2;
}

functionTopClass() {}
TopClass.inherits(SubClass);
TopClass.prototype.getName =function() {
//Looksprettycleananditcalls
//thegetName()methodofSubClass
return"TopClass("+this.getId() +")extends"+
this.uber("getName");
}

TopClass.prototype.getId =function() {
//Ok,sothis.getId()shouldalways
//returntheresultofcallinggetId()
//onSubClass,whichis2.Sodoesit?
returnthis.uber("getId");
}

//Alerts"TopClass(2)extendsSubClass(1)extendsBaseClass(1)"
//Hmm...this.getId()didn'talwaysreturn2.
//Whathappened?
alert(newTopClass().getName());

The first section includes Crockford's 'inherit' and 'uber' method code verbatim. The next section looks very much like the previous examples, except that I've now added a third level of inheritance to illustrate an issue that exists in Crockford's approach. Admittedly, Crockford's approach is one of the most solid that I found and there is no arguing that based on the body of work he has done on JavaScript programming, he is a master of JavaScript. However, if you review the code for the three classes and then look at the output, you'll notice that there is subtle issue.

The output reflects that this.getId() returned the correct value of '2' for TopClass, but it returned '1' instead of '2' when called from the getName() method of SubClass and BaseClass. As you can see, the getName() super calls behaved correctly and the three class names are displayed accurately. It's only when the additional this.uber("getId") super call is interjected into the call stack that a problem is revealed. Since the object is a TopClass instance, every call to this.getId() in the call stack should always result in the getId() method on TopClass being called, which it does. The problem is that the getId() method on TopClass performs a super class call via this.uber("getId"), and that call incorrectly invokes the getId() method of BaseClass in two out of the three cases, thus resulting in '1' being displayed twice. The correct behavior is for the getId() method of SubClass() to be called in all three cases and for a value of '2' to be displayed three times.

That was kinda tricky to describe, and I'm not sure I explained it clearly. However at the very least, it should be clear that the results of the above example are incorrect. Additionally, a downside of Crockford's approach and of many other approaches, is that every super call requires an additional method call and additional processing of some kind. Whether this is an issue for your situation, will depend on how many super calls you have in your code. ThinWire makes extensive use of super calling in it's client-side code and therefore it's important that super calls are as fast as possible.

With Crockford's approach broken and with no luck finding a working approach on the web, I set out to see if I could devise a working design of my own. It took me about a week to work out something that behaved correctly under all situations, but once I felt confident that it was working, I quickly integrated the approach into the framework. As it stands now, both the beta and beta2 releases of ThinWire leverage that initial design.

Dynamic Super Calling That Works:

One Time Support Code
//DefinesthetoplevelClass
functionClass() {}
Class.prototype.construct =function() {};
Class.__asMethod__ =function(func, superClass) {
returnfunction() {
var currentSuperClass =this.$;
this.$ = superClass;
var ret = func.apply(this, arguments);
this.$ = currentSuperClass;
return ret;
};
};

Class.extend =function(def) {
var classDef =function() {
if (arguments[0]!== Class) {this.construct.apply(this, arguments);}
};

var proto =newthis(Class);
var superClass =this.prototype;

for (var n in def) {
var item = def[n];

if (item instanceofFunction) {
item = Class.__asMethod__(item, superClass);
}

proto[n]= item;
}

proto.$ = superClass;
classDef.prototype = proto;

//Givethisnewclassthesamestaticextendmethod
classDef.extend =this.extend;
return classDef;
};

Working Example
//Hey,thisclassdefinitionapproach
//looksmuchcleanerthanthenothers.
var BaseClass = Class.extend({
construct: function() {/*optionalconstructormethod*/},

getName: function() {
return"BaseClass("+this.getId() +")";
},

getId: function() {
return1;
}
});

var SubClass = BaseClass.extend({
getName: function() {
//CallsthegetName()methodofBaseClass
return"SubClass("+this.getId() +")extends"+
this.$.getName.call(this);
},

getId: function() {
return2;
}
});

var TopClass = SubClass.extend({
getName: function() {
//CallsthegetName()methodofSubClass
return"TopClass("+this.getId() +")extends"+
this.$.getName.call(this);
},

getId: function() {
//Muchbetter,this.getId()always
//returnstheresultofcallinggetId()
//onSubClass,whichis2.
returnthis.$.getId.call(this);
}
});

//Alerts"TopClass(2)extendsSubClass(2)extendsBaseClass(2)"
//Everythinglooksgood!
alert(newTopClass().getName());

There's a lot going on in that example, but in general this approach includes a fairly clean class definition model and proper super call semantics, which are established by the 'extend' method. Essentially, 'extend' wraps every method in your class definition with an intermediate function that swaps the super class reference '$' to the correct reference with every method call. The only real issue with this approach is that it requires a number of intermediate functions, which may have an impact on performance. So recently I revisited the design and came up with an improved approach that eliminates the intermediate functions entirely.

Dynamic Super Calling That's Faster and Works:

One Time Support Code
//DefinesthetoplevelClass
functionClass() {}
Class.prototype.construct =function() {};
Class.extend =function(def) {
var classDef =function() {
if (arguments[0]!== Class) {this.construct.apply(this, arguments);}
};

var proto =newthis(Class);
var superClass =this.prototype;

for (var n in def) {
var item = def[n];
if (item instanceofFunction) item.$ = superClass;
proto[n]= item;
}

classDef.prototype = proto;

//Givethisnewclassthesamestaticextendmethod
classDef.extend =this.extend;
return classDef;
};

Working Example
//Hey,thisclassdefinitionapproach
//looksmuchcleanerthanthenothers.
var BaseClass = Class.extend({
construct: function() {/*optionalconstructormethod*/},

getName: function() {
return"BaseClass("+this.getId() +")";
},

getId: function() {
return1;
}
});

var SubClass = BaseClass.extend({
getName: function() {
//CallsthegetName()methodofBaseClass
return"SubClass("+this.getId() +")extends"+
arguments.callee.$.getName.call(this);
},

getId: function() {
return2;
}
});

var TopClass = SubClass.extend({
getName: function() {
//CallsthegetName()methodofSubClass
return"TopClass("+this.getId() +")extends"+
arguments.callee.$.getName.call(this);
},

getId: function() {
//Justlikethelastexample,this.getId()
//alwaysreturnsthepropervalueof2.
return arguments.callee.$.getId.call(this);
}
});

//Alerts"TopClass(2)extendsSubClass(2)extendsBaseClass(2)"
//Looksgoodagain,andthere'snointermediatefunctions!
alert(newTopClass().getName());


This final design leverages a little known feature in JavaScript, although one that is supported by all major browsers. During the execution of any function, you can refer to the arguments that were passed in via the 'arguments' array. That's fairly well known, but the lesser known detail is that the 'arguments' array contains a reference in the property 'callee', which points to the current function that is being executed. This is important, because it's the only way that you can get such a reference since the function reference available via the 'this' object, always refers to the overridden function that is defined in the class at the top of the hierarchy.

That's About It:

Ok, so that was a bit exhaustive. But I wanted to document the details in case anyone is curious and so that I'll have something to refer to if I ever need to refresh my memory. In any case, I welcome your comments and any suggestions you may have.

原文链接地址:
http://truecode.blogspot.com/2006/08/object-oriented-super-class-method.html