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

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

Ok,這便是相對徹底的問題解決方案了。但是我想通過把它的細節寫成文檔讓每個人閱讀,以致我可以從中找出漏洞不斷地完善代碼。歡迎對我的文章進行評論和建議!