要用 ECMAScript 實(shí)現(xiàn)繼承機(jī)制,您可以從要繼承的基類入手。所有開發(fā)者定義的類都可作為基類。出于安全原因,本地類和宿主類不能作為基類,這樣可以防止公用訪問編譯過的瀏覽器級的代碼,因?yàn)檫@些代碼可以被用于惡意攻擊。
選定基類后,就可以創(chuàng)建它的子類了。是否使用基類完全由你決定。有時(shí),你可能想創(chuàng)建一個(gè)不能直接使用的基類,它只是用于給子類提供通用的函數(shù)。在這種情況下,基類被看作抽象類。
盡管 ECMAScript 并沒有像其他語言那樣嚴(yán)格地定義抽象類,但有時(shí)它的確會(huì)創(chuàng)建一些不允許使用的類。通常,我們稱這種類為抽象類。
創(chuàng)建的子類將繼承超類的所有屬性和方法,包括構(gòu)造函數(shù)及方法的實(shí)現(xiàn)。記住,所有屬性和方法都是公用的,因此子類可直接訪問這些方法。子類還可添加超類中沒有的新屬性和方法,也可以覆蓋超類的屬性和方法。
和其他功能一樣,ECMAScript 實(shí)現(xiàn)繼承的方式不止一種。這是因?yàn)?JavaScript 中的繼承機(jī)制并不是明確規(guī)定的,而是通過模仿實(shí)現(xiàn)的。這意味著所有的繼承細(xì)節(jié)并非完全由解釋程序處理。作為開發(fā)者,你有權(quán)決定最適用的繼承方式。
下面為您介紹幾種具體的繼承方式。
構(gòu)想原始的 ECMAScript 時(shí),根本沒打算設(shè)計(jì)對象冒充(object masquerading)。它是在開發(fā)者開始理解函數(shù)的工作方式,尤其是如何在函數(shù)環(huán)境中使用 this 關(guān)鍵字后才發(fā)展出來。
其原理如下:構(gòu)造函數(shù)使用 this 關(guān)鍵字給所有屬性和方法賦值(即采用類聲明的構(gòu)造函數(shù)方式)。因?yàn)闃?gòu)造函數(shù)只是一個(gè)函數(shù),所以可使 ClassA 構(gòu)造函數(shù)成為 ClassB 的方法,然后調(diào)用它。ClassB 就會(huì)收到 ClassA 的構(gòu)造函數(shù)中定義的屬性和方法。例如,用下面的方式定義 ClassA 和 ClassB:
function ClassA(sColor) { this.color = sColor; this.sayColor = function () { alert(this.color); }; } function ClassB(sColor) { }
還記得嗎?關(guān)鍵字 this 引用的是構(gòu)造函數(shù)當(dāng)前創(chuàng)建的對象。不過在這個(gè)方法中,this 指向的所屬的對象。這個(gè)原理是把 ClassA 作為常規(guī)函數(shù)來建立繼承機(jī)制,而不是作為構(gòu)造函數(shù)。如下使用構(gòu)造函數(shù) ClassB 可以實(shí)現(xiàn)繼承機(jī)制:
function ClassB(sColor) { this.newMethod = ClassA; this.newMethod(sColor); delete this.newMethod; }
在這段代碼中,為 ClassA 賦予了方法 newMethod(請記住,函數(shù)名只是指向它的指針)。然后調(diào)用該方法,傳遞給它的是 ClassB 構(gòu)造函數(shù)的參數(shù) sColor。最后一行代碼刪除了對 ClassA 的引用,這樣以后就不能再調(diào)用它。
所有新屬性和新方法都必須在刪除了新方法的代碼行后定義。否則,可能會(huì)覆蓋超類的相關(guān)屬性和方法:
function ClassB(sColor, sName) { this.newMethod = ClassA; this.newMethod(sColor); delete this.newMethod; this.name = sName; this.sayName = function () { alert(this.name); }; }
為證明前面的代碼有效,可以運(yùn)行下面的例子:
var objA = new ClassA("blue"); var objB = new ClassB("red", "John"); objA.sayColor(); //輸出 "blue" objB.sayColor(); //輸出 "red" objB.sayName(); //輸出 "John"
有趣的是,對象冒充可以支持多重繼承。也就是說,一個(gè)類可以繼承多個(gè)超類。用 UML 表示的多重繼承機(jī)制如下圖所示:
例如,如果存在兩個(gè)類 ClassX 和 ClassY,ClassZ 想繼承這兩個(gè)類,可以使用下面的代碼:
function ClassZ() { this.newMethod = ClassX; this.newMethod(); delete this.newMethod; this.newMethod = ClassY; this.newMethod(); delete this.newMethod; }
這里存在一個(gè)弊端,如果存在兩個(gè)類 ClassX 和 ClassY 具有同名的屬性或方法,ClassY 具有高優(yōu)先級。因?yàn)樗鼜暮竺娴念惱^承。除這點(diǎn)小問題之外,用對象冒充實(shí)現(xiàn)多重繼承機(jī)制輕而易舉。
由于這種繼承方法的流行,ECMAScript 的第三版為 Function 對象加入了兩個(gè)方法,即 call() 和 apply()。
call() 方法是與經(jīng)典的對象冒充方法最相似的方法。它的第一個(gè)參數(shù)用作 this 的對象。其他參數(shù)都直接傳遞給函數(shù)自身。例如:
function sayColor(sPrefix,sSuffix) { alert(sPrefix + this.color + sSuffix); }; var obj = new Object(); obj.color = "blue"; sayColor.call(obj, "The color is ", "a very nice color indeed.");
在這個(gè)例子中,函數(shù) sayColor() 在對象外定義,即使它不屬于任何對象,也可以引用關(guān)鍵字 this。對象 obj 的 color 屬性等于 blue。調(diào)用 call() 方法時(shí),第一個(gè)參數(shù)是 obj,說明應(yīng)該賦予 sayColor() 函數(shù)中的 this 關(guān)鍵字值是 obj。第二個(gè)和第三個(gè)參數(shù)是字符串。它們與 sayColor() 函數(shù)中的參數(shù) sPrefix 和 sSuffix 匹配,最后生成的消息 "The color is blue, a very nice color indeed." 將被顯示出來。
要與繼承機(jī)制的對象冒充方法一起使用該方法,只需將前三行的賦值、調(diào)用和刪除代碼替換即可:
function ClassB(sColor, sName) { //this.newMethod = ClassA; //this.newMethod(sColor); //delete this.newMethod; ClassA.call(this, sColor); this.name = sName; this.sayName = function () { alert(this.name); }; }
這里,我們需要讓 ClassA 中的關(guān)鍵字 this 等于新創(chuàng)建的 ClassB 對象,因此 this 是第一個(gè)參數(shù)。第二個(gè)參數(shù) sColor 對兩個(gè)類來說都是唯一的參數(shù)。
apply() 方法有兩個(gè)參數(shù),用作 this 的對象和要傳遞給函數(shù)的參數(shù)的數(shù)組。例如:
function sayColor(sPrefix,sSuffix) { alert(sPrefix + this.color + sSuffix); }; var obj = new Object(); obj.color = "blue"; sayColor.apply(obj, new Array("The color is ", "a very nice color indeed."));
這個(gè)例子與前面的例子相同,只是現(xiàn)在調(diào)用的是 apply() 方法。調(diào)用 apply() 方法時(shí),第一個(gè)參數(shù)仍是 obj,說明應(yīng)該賦予 sayColor() 函數(shù)中的 this 關(guān)鍵字值是 obj。第二個(gè)參數(shù)是由兩個(gè)字符串構(gòu)成的數(shù)組,與 sayColor() 函數(shù)中的參數(shù) sPrefix 和 sSuffix 匹配,最后生成的消息仍是 "The color is blue, a very nice color indeed.",將被顯示出來。
該方法也用于替換前三行的賦值、調(diào)用和刪除新方法的代碼:
function ClassB(sColor, sName) { //this.newMethod = ClassA; //this.newMethod(color); //delete this.newMethod; ClassA.apply(this, new Array(sColor)); this.name = sName; this.sayName = function () { alert(this.name); }; }
同樣的,第一個(gè)參數(shù)仍是 this,第二個(gè)參數(shù)是只有一個(gè)值 color 的數(shù)組??梢园?ClassB 的整個(gè) arguments 對象作為第二個(gè)參數(shù)傳遞給 apply() 方法:
function ClassB(sColor, sName) { //this.newMethod = ClassA; //this.newMethod(color); //delete this.newMethod; ClassA.apply(this, arguments); this.name = sName; this.sayName = function () { alert(this.name); }; }
當(dāng)然,只有超類中的參數(shù)順序與子類中的參數(shù)順序完全一致時(shí)才可以傳遞參數(shù)對象。如果不是,就必須創(chuàng)建一個(gè)單獨(dú)的數(shù)組,按照正確的順序放置參數(shù)。此外,還可使用 call() 方法。
繼承這種形式在 ECMAScript 中原本是用于原型鏈的。上一章介紹了定義類的原型方式。原型鏈擴(kuò)展了這種方式,以一種有趣的方式實(shí)現(xiàn)繼承機(jī)制。
在上一章學(xué)過,prototype 對象是個(gè)模板,要實(shí)例化的對象都以這個(gè)模板為基礎(chǔ)??偠灾?,prototype 對象的任何屬性和方法都被傳遞給那個(gè)類的所有實(shí)例。原型鏈利用這種功能來實(shí)現(xiàn)繼承機(jī)制。
如果用原型方式重定義前面例子中的類,它們將變?yōu)橄铝行问剑?/p>
function ClassA() {
}
ClassA.prototype.color = "blue";
ClassA.prototype.sayColor = function () {
alert(this.color);
};
function ClassB() {
}
ClassB.prototype = new ClassA();
原型方式的神奇之處在于“ClassB.prototype = new ClassA()”代碼行。這里,把 ClassB 的 prototype 屬性設(shè)置成 ClassA 的實(shí)例。這很有意思,因?yàn)橄胍?ClassA 的所有屬性和方法,但又不想逐個(gè)將它們 ClassB 的 prototype 屬性進(jìn)行手動(dòng)鏈接。還有比把 ClassA 的實(shí)例賦予 prototype 屬性更好的方法嗎?
注意:調(diào)用 ClassA 的構(gòu)造函數(shù),沒有給它傳遞參數(shù)。這在原型鏈中是標(biāo)準(zhǔn)做法。要確保構(gòu)造函數(shù)沒有任何參數(shù)。
與對象冒充相似,子類的所有屬性和方法都必須出現(xiàn)在 prototype 屬性被賦值后,因?yàn)樵谒百x值的所有方法都會(huì)被刪除。為什么?因?yàn)?prototype 屬性被替換成了新對象,添加了新方法的原始對象將被銷毀。所以,為 ClassB 類添加 name 屬性和 sayName() 方法的代碼如下:
function ClassB() { } ClassB.prototype = new ClassA(); ClassB.prototype.name = ""; ClassB.prototype.sayName = function () { alert(this.name); };
可通過運(yùn)行下面的例子測試這段代碼:
var objA = new ClassA(); var objB = new ClassB(); objA.color = "blue"; objB.color = "red"; objB.name = "John"; objA.sayColor(); objB.sayColor(); objB.sayName();
此外,在原型鏈中,instanceof 運(yùn)算符的運(yùn)行方式也很獨(dú)特。對 ClassB 的所有實(shí)例,instanceof 為 ClassA 和 ClassB 都返回 true。例如:
var objB = new ClassB(); alert(objB instanceof ClassA); //輸出 "true" alert(objB instanceof ClassB); //輸出 "true"
在 ECMAScript 的弱類型世界中,這是極其有用的工具,不過使用對象冒充時(shí)不能使用它。
原型鏈的弊端是不支持多重繼承。記住,原型鏈會(huì)用另一類型的對象重寫類的 prototype 屬性。
這種繼承方式使用構(gòu)造函數(shù)定義類,并非使用任何原型。對象冒充的主要問題是必須使用構(gòu)造函數(shù)方式,這不是最好的選擇。不過如果使用原型鏈,就無法使用帶參數(shù)的構(gòu)造函數(shù)了。開發(fā)者如何選擇呢?答案很簡單,兩者都用。
在前一章,我們曾經(jīng)講解過創(chuàng)建類的最好方式是用構(gòu)造函數(shù)定義屬性,用原型定義方法。這種方式同樣適用于繼承機(jī)制,用對象冒充繼承構(gòu)造函數(shù)的屬性,用原型鏈繼承 prototype 對象的方法。用這兩種方式重寫前面的例子,代碼如下:
function ClassA(sColor) { this.color = sColor; } ClassA.prototype.sayColor = function () { alert(this.color); }; function ClassB(sColor, sName) {ClassA.call(this, sColor);
this.name = sName; }ClassB.prototype = new ClassA();
ClassB.prototype.sayName = function () { alert(this.name); };
在此例子中,繼承機(jī)制由兩行突出顯示的藍(lán)色代碼實(shí)現(xiàn)。在第一行突出顯示的代碼中,在 ClassB 構(gòu)造函數(shù)中,用對象冒充繼承 ClassA 類的 sColor 屬性。在第二行突出顯示的代碼中,用原型鏈繼承 ClassA 類的方法。由于這種混合方式使用了原型鏈,所以 instanceof 運(yùn)算符仍能正確運(yùn)行。
下面的例子測試了這段代碼:
var objA = new ClassA("blue"); var objB = new ClassB("red", "John"); objA.sayColor(); //輸出 "blue" objB.sayColor(); //輸出 "red" objB.sayName(); //輸出 "John"
更多建議: