幾乎每位在開發(fā)JavaScript時嘗試應(yīng)用面向?qū)ο蠹夹g(shù)的開發(fā)者,或多或少都會問自己一個問題:“如何調(diào)用父類(super class)的方法?”在Ajax技術(shù)還沒有目前這樣炙手可熱之前,這種問題很少出現(xiàn),因為大多數(shù)開發(fā)者僅在進行客戶端form驗證或者簡單的 DHTML/DOM操作時使用JavaScript。在那些簡單的解決方案中,
函數(shù)式編程(functional programming)是很有意義的,面向?qū)ο缶幊虅t處在次之重要的位置。現(xiàn)在,Ajax技術(shù)發(fā)展勢頭迅猛,開發(fā)者已經(jīng)建立了一個調(diào)用大量客戶端JavaScript、不斷增長的、復(fù)雜的系統(tǒng)。因此,在JavaScript上嘗試OO技術(shù)便成為了管理復(fù)雜性的一種手段。在此過程中,多數(shù)開發(fā)者很快便認識到:JavaScript 是一種原型化的(prototypical)語言,它缺少OO自身帶來的多種便利。
????????OO設(shè)計的主旨和關(guān)于它的一些話題談起來很大,但只著眼于Class的定義方式,我認為它是JavaScript開發(fā)者嘗試解決問題的首選。因此,你可以在互聯(lián)網(wǎng)上找到許多不同的問題解決案例,但在我看過它們后不免有些失望——這些案例都是在某個場合下適用,而不是放之四海而皆準的通法。而我對這個話題的興趣來自于我的team在開發(fā)
ThinWire Ajax Framework的影響。由于這個框架生成出對客戶端代碼的需求,才使我們“被迫”去實現(xiàn)可靠的、支持父類方法調(diào)用的OO模式。通過父類調(diào)用,你可以進一步依靠類的繼承特性來核心化通用代碼,從而更易于減少重復(fù)代碼,去掉客戶端
代碼的壞味道。
????????下面羅列出了一些在我的研究過程中遇到的解決方式。最終,我沒有從中找出一個可以接收的解決方案,于是我不得不實現(xiàn)一個自己的解決方案,你將在本文的結(jié)尾部分看到這個方案。
????????然而父類調(diào)用在這里是最重要的OO機制,因此我需要一個相應(yīng)的工作模式,也正是因為在我的觀點中原型化方式是丑陋的,所以我更需要一種更加自然地使用JavaScript定義類的方法。
More Solutions:????????好吧,讓我們進入討論。正如開發(fā)者所察覺的那樣,在JS中實現(xiàn)基本的繼承是很容易的事,事實上有一些眾所周知的方法:
丑陋的Solution:????????
沒有進行父類調(diào)用的簡單繼承:
// 提前寫好的JavaScript Class定義和繼承
// 當然,這種代碼很丑陋,散發(fā)著代碼的壞味道。
function BaseClass() {
????//BaseClass constructor code goes here
}
BaseClass.prototype.getName = function() {
????return "BaseClass";
}
function SubClass() {
????//SubClass constructor code goes here
}
//Inherit the methods of BaseClass
SubClass.prototype = new BaseClass();
//Override the parent's getName method
SubClass.prototype.getName = function() {
????return "SubClass";
}
//Alerts "SubClass"
alert(new SubClass().getName());
導(dǎo)致IE內(nèi)存泄露的Solution:????????
這種實現(xiàn)方式能夠?qū)е略贗E中的內(nèi)存泄漏,你應(yīng)該盡量避免:
// 運行時的JavaScript Class 定義和繼承
// 看上去很傳統(tǒng),但這些腳本會導(dǎo)致在Internet Explorer中的內(nèi)存泄漏.
function BaseClass() {
????this.getName = function() {
????????return "BaseClass";
????};????
????//BaseClass constructor code goes here
}
function SubClass() {
????//在對象實例建立時重載父類的getName方法
????this.getName = function() {
????????return "SubClass";
????}
????//SubClass constructor code goes here
}
//Inherit the methods of BaseClass
SubClass.prototype = new BaseClass();
//Alerts "SubClass"
alert(new SubClass().getName());
????????
????????就像我在第一個實現(xiàn)方法中所注釋的那樣,第一個實現(xiàn)方法有些丑陋,但它相比引起內(nèi)存泄漏的第二種方式便是首選了。
????????我把這兩種方法放在這里的目的是指出你不應(yīng)該使用它們。
硬性編碼的Solution:????????讓我們看一下第一個例子,它采用了標準的原型化方式,但問題是:它的子類方法如何調(diào)用父類(基類)方法?下面是一些開發(fā)者嘗試并采用的方式:
????????
一種企圖進行父類調(diào)用的“通病”:
function BaseClass() { }
BaseClass.prototype.getName = function() {
????return "BaseClass(" + this.getId() + ")";
}
BaseClass.prototype.getId = function() {
????return 1;
}
function SubClass() {}
SubClass.prototype = new BaseClass();
SubClass.prototype.getName = function() {
????//調(diào)用父類的getName()方法
????//哈哈,這是對父類調(diào)用的直接引用嗎?
????return "SubClass(" + this.getId() + ") extends " +
????????BaseClass.prototype.getName();
}
SubClass.prototype.getId = function() {
????return 2;
}
//輸出結(jié)果:"SubClass(2) extends BaseClass(1)";
//這是正確的輸出嗎?
alert(new SubClass().getName());
???????? 上面的代碼是對第一段腳步進行修改后的版本,我去掉了一些注釋和空格,使你能注意到新的getId()方法和對父類的調(diào)用。你一定急于知道通過這樣對 BaseClass的硬性編碼引用(hard coded reference),它是否能進行正確地調(diào)用BaseClass的方法?
???????? 一個正確的、多態(tài)的父類調(diào)用必做的事情是保證“this”引用指向當前對象實例和類方法。在這里,看上去和它應(yīng)該輸出的結(jié)果非常接近,看上去好像在 SubClass中調(diào)用了BaseClass的getName()方法。你發(fā)現(xiàn)問題了嗎?這個問題是非常細小的,但卻很重要決不能忽視。通過使用上面的父類調(diào)用語法,BaseClass的getName()方法被調(diào)用,它返回一個字符串:包括類名和“this.getId()”的返回值。問題在于 “this.getId()”應(yīng)該返回2,而不是1。如果這和你所想的不同,你可以查看Java或者C#這類OO語言的多態(tài)性。
改進后的硬性編碼Solution:????????你可以通過一個微小的改動來解決這個問題。
靜態(tài)(硬編碼)父類調(diào)用:
function BaseClass() { }
BaseClass.prototype.getName = function() {
????return "BaseClass(" + this.getId() + ")";
}
BaseClass.prototype.getId = function() {
????return 1;
}
function SubClass() {}
SubClass.prototype = new BaseClass();
SubClass.prototype.getName = function() {
????//一點魔法加上多態(tài)性!
????//但很明顯,這還是一個直接引用!????
????return "SubClass(" + this.getId() + ") extends " +
????????BaseClass.prototype.getName.call(this);
}
SubClass.prototype.getId = function() {
????return 2;
}
//輸出結(jié)果:"SubClass(2) extends BaseClass(2)";
//Hey, 我們得到了正確的輸出!
alert(new SubClass().getName());
????????在
ECMA-262 JavaScript/EcmaScript標準中, Call()方法是所有Function實例的一個成員方法,這已經(jīng)被所有的主流瀏覽器所支持。JavaScript把所有的function看作對象,因此每個function都具有方法和附著其上的屬性。Call()方法允許你調(diào)用某個function,并在function的調(diào)用過程中確定 “this”變量應(yīng)該是什么。JavaScript的function沒有被緊緊地綁定到它所在的對象上,所以如果你沒有顯式地使用call()方法, “this”變量將成為function所在的對象。
????????另外一種方法是使用apply方法,它和call()方法類似,只在參數(shù)上存在不同:apply()方法接受參數(shù)的數(shù)組,而call()方法接受單個參數(shù)。
Douglas Crockford的Solution:????????現(xiàn)在回溯到上面的示例,在這個示例中唯一的問題就是父類引用是直接的、硬性編寫的。它可以適用于小型的類繼承環(huán)境,但對于具有較深層次的大型繼承來講,這些直接引用非常難于維護。
????????那么,有解決方法嗎?不幸的是這里沒有簡單的解決方案。
????????JavaScript沒有提供對通過“隱性引用”方式調(diào)用父類方法的支持,這里也沒有在其它OO語言中使用的“super”變量的等價物。于是,一些開發(fā)者做出了自己的解決方案,但就像我前面提到的那樣,每個解決方案都存在某種缺點。
????????例如,下面列出的眾多著名方法之一:JavaScript大師
[ur=http://en.wikipedia.org/wiki/Douglas_Crockford]Douglas Crockford[/url]在他的
《Classical Inheritance in JavaScript》中提出的方法。
????????Douglas Crockford的方法在多數(shù)情況下可以正常工作:
一次性支持代碼:
//Crockford的方法:給所有的function都增加'inherits' 方法、
//每個類都增加了'uber'方法來調(diào)用父類方法
Function.prototype.inherits = function(parent) {
????var d = 0, p = (this.prototype = new parent());
????
????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;
????};
};
運行示例:
function BaseClass() { }
BaseClass.prototype.getName = function() {
????return "BaseClass(" + this.getId() + ")";
}
BaseClass.prototype.getId = function() {
????return 1;
}
function SubClass() {}
SubClass.inherits(BaseClass);
SubClass.prototype.getName = function() {
????//這里看上去非常的清晰,它調(diào)用了BaseClass的getName()方法
return "SubClass(" + this.getId() + ") extends " +
????????this.uber("getName");
}
SubClass.prototype.getId = function() {
????return 2;
}
function TopClass() {}
TopClass.inherits(SubClass);
TopClass.prototype.getName = function() {
????//這里看上去非常的清晰,它調(diào)用了SubClass的getName()方法
????return "TopClass(" + this.getId() + ") extends " +
????????this.uber("getName");
}
TopClass.prototype.getId = function() {
????//Ok, 因此this.getId()應(yīng)該總是
//返回調(diào)用SubClass的getId()方法的返回值(2)。
????return this.uber("getId");
}
//輸出結(jié)果:"TopClass(2) extends SubClass(1) extends BaseClass(1)"
//嗯?后面的兩次this.getId()調(diào)用都沒有返回2.
//發(fā)生了什么?
alert(new TopClass().getName());
????????
???????? 上面代碼的第一部分包括了Crockford的“inherit”和“uber”方法代碼。第二部分看上去和前面的示例很類似,除了我添加了用來演示 Crockford方式所存在問題的第三層繼承關(guān)系。誠然,Crockford這位JavaScript大師的方法是我所找到的最可靠的方法之一,我很敬佩他在JavaScript編程方面做出的貢獻。但是,如果你使用三個依次繼承的類來考核他的代碼,你將從輸出中發(fā)現(xiàn)這里存在著細微的問題。
???????? 從輸出結(jié)果看,第一次調(diào)用的this.getId()返回了TopClass當前的id值“2”,但在調(diào)用SubClass和BaseClass的 getName()方法時返回了“1”而不是“2”。從代碼上看,在getName()方法中的父類調(diào)用行為是正確的,三個類的名字都被正確地顯示出來。唯一的問題出現(xiàn)在this.uber("getId")這個父類調(diào)用被放入調(diào)用堆棧(call stack)時。因為此時當前對象是一個TopClass實例,而每次調(diào)用在調(diào)用堆棧中的this.getId()都應(yīng)該返回調(diào)用TopClass的 getId()方法后的返回值。
????????而問題是TopClass的this.getId()方法通過this.uber ("getId")執(zhí)行了父類調(diào)用,這三次this.getId()調(diào)用中的后兩次錯誤地調(diào)用了BaseClass的getId()方法,這樣便在輸出結(jié)果中顯示了兩次“1”。正確的行為應(yīng)該是調(diào)用三次SubClass的getId()方法,在輸出結(jié)果中顯示三次“2”。大家可以通過
FireFox的FireBug插件進行代碼debug進行觀察。
????????這是十分難以描述的現(xiàn)象,我不能保證我能把它解釋清楚。但是至少從上面的運行結(jié)果中可以看出它是錯誤的。
????????另外,Crockford的方法和其它一些方法的劣勢在于每個父類調(diào)用都需要一個額外的方法調(diào)用和額外的某種處理。這是否成為你所面臨的問題,取決于你所使用的父類調(diào)用深度。在
ThinWire項目的客戶端代碼中使用了大量的父類調(diào)用,因此父類調(diào)用的可靠性和快速性在項目中是很重要的。
我的初級Solution:????????面對這樣的窘境——
Crockford的方法出現(xiàn)問題、在互聯(lián)網(wǎng)上沒有找到符合要求的方法,我決定看看我自己是否可以發(fā)明一種可以滿足要求的方法。這花掉了我近一周的時間來使代碼工作并滿足各種情況,但我對它的工作情況很有信心,并且很快把它與framework集成在一起,TinWire的beta和beta2兩個版本中都使用了這些“初級設(shè)計”的代碼。
????????動態(tài)父類調(diào)用:
一次性支持代碼:
//定義最頂級類
function Class() { }
Class.prototype.construct = function() {};
Class.__asMethod__ = function(func, superClass) {????
????return function() {
????????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 = new this(Class);
????var superClass = this.prototype;
????
????for (var n in def) {
????????var item = def[n];????????????????????????
????????
????????if (item instanceof Function) {
????????????item = Class.__asMethod__(item, superClass);
????????}
????????
????????proto[n] = item;
????}
????proto.$ = superClass;
????classDef.prototype = proto;
????
????//賦給這個新的子類同樣的靜態(tài)extend方法
????classDef.extend = this.extend;????????
????return classDef;
};
運行示例:
//Hey, 注意一下這個類的定義方式
//看上去比其它方式要清楚些
var BaseClass = Class.extend({
????construct: function() { /* optional constructor method */ },
????
????getName: function() {
????????return "BaseClass(" + this.getId() + ")";
????},
????
????getId: function() {
????????return 1;
????}
});
var SubClass = BaseClass.extend({
????getName: function() {
????????//調(diào)用BaseClass的getName()方法
????????return "SubClass(" + this.getId() + ") extends " +
????????????this.$.getName.call(this);
????},
????
????getId: function() {
????????return 2;
????}
});
var TopClass = SubClass.extend({
????getName: function() {
????????//調(diào)用SubClass的getName()方法
????????return "TopClass(" + this.getId() + ") extends " +
????????????this.$.getName.call(this);
????},
????
????getId: function() {
????????//this.getId()總是返回調(diào)用父類的getId()方法的返回值(2)
????????return this.$.getId.call(this);
????}
});
//輸出結(jié)果:"TopClass(2) extends SubClass(2) extends BaseClass(2)"
//一切都正確!
alert(new TopClass().getName());
???????? 這里是前面示例的,但是目前這種方式包括了通過“extend”方法實現(xiàn)的十分清晰的類定義模式和正確的父類調(diào)用語義。尤其是“extend”方法通過一個中間function封裝了類定義中的每個方法,這個中間function在每次方法調(diào)用時首先把當前父類引用“$” 與正確的父類引用相互交換,然后把這個正確的父類引用傳遞給apply()進行方法調(diào)用,最后再將把當前父類引用“$” 與正確的父類引用交換回來。這種方式唯一的問題就是它需要一些中間function,它們會對性能產(chǎn)生不良影響。所以近來我重新審視了設(shè)計、完成了去掉了中間function了一種改良的方式。
改良后的Solution:????????動態(tài)父類調(diào)用快速版本:
一次性支持代碼
//定義最頂級類
function Class() { }
Class.prototype.construct = function() {};
Class.extend = function(def) {
????var classDef = function() {
????????if (arguments[0] !== Class) { this.construct.apply(this, arguments); }
????};
????
????var proto = new this(Class);
????var superClass = this.prototype;
????
????for (var n in def) {
????????var item = def[n];????????????????????????
????????if (item instanceof Function) item.$ = superClass;
????????proto[n] = item;
????}
????classDef.prototype = proto;
????
????//賦給這個新的子類同樣的靜態(tài)extend方法
????classDef.extend = this.extend;????????
????return classDef;
};
運行示例:
//Hey, 注意一下這個類的定義方式
//看上去比其它方式要清楚些
var BaseClass = Class.extend({
????construct: function() { /* optional constructor method */ },
????????
????getName: function() {
????????return "BaseClass(" + this.getId() + ")";
????},
????
????getId: function() {
????????return 1;
????}
});
var SubClass = BaseClass.extend({
????getName: function() {
????????//調(diào)用BaseClass的getName()方法
????????return "SubClass(" + this.getId() + ") extends " +
????????????arguments.callee.$.getName.call(this);
????},
????
????getId: function() {
????????return 2;
????}
});
var TopClass = SubClass.extend({
????getName: function() {
????????//調(diào)用SubClass的getName()方法
????????return "TopClass(" + this.getId() + ") extends " +
????????????arguments.callee.$.getName.call(this);
????},
????
????getId: function() {
????????// this.getId()總是返回調(diào)用父類的getId()方法的返回值(2)
????????return arguments.callee.$.getId.call(this);
????}
});
//輸出結(jié)果:"TopClass(2) extends SubClass(2) extends BaseClass(2)"
//工作正常!而且沒有任何中間function
alert(new TopClass().getName());
????????這是最后的設(shè)計,它使用了JavaScript中一點鮮為人知的特性:callee。
???????? 在任何方法執(zhí)行過程中,你可以查看那些通過“arguments”數(shù)組傳入的參數(shù),這是眾所周知的,但很少有人知道“arguments”數(shù)組包含一個名為“callee”的屬性,它作為一個引用指向了當前正在被執(zhí)行的function,而后通過“$”便可以方便的獲得當前被執(zhí)行function所在類的父類。這是非常重要的,因為它是獲得此引用的唯一途徑(通過“this”對象獲得的function引用總是指向被子類重載的function,而后者并非全是正在被執(zhí)行的function)。