1.繼承(接口繼承和實(shí)現(xiàn)繼承)
繼承是 OO 語言中的一個最為人津津樂道的概念。許多 OO 語言都支持兩種繼承方式:接口繼承和實(shí)現(xiàn)繼承。接口繼承只繼承方法簽名,而實(shí)現(xiàn)繼承則繼承實(shí)際的方法。如前所述,由于函數(shù)沒有簽名,在 ECMAScript 中無法實(shí)現(xiàn)接口繼承。 ECMAScript 只支持實(shí)現(xiàn)繼承,而且其實(shí)現(xiàn)繼承主要是依靠原型鏈來實(shí)現(xiàn)的。
2.實(shí)現(xiàn)繼承的幾種方式:
方式一:(原型鏈)
ECMAScript 中描述了原型鏈的概念,并將原型鏈作為實(shí)現(xiàn)繼承的主要方法。其基本思想是利用原型讓一個引用類型繼承另一個引用類型的屬性和方法。簡單回顧一下構(gòu)造函數(shù)、原型和實(shí)例的關(guān)系:每個構(gòu)造函數(shù)都有一個原型對象,原型對象都包含一個指向構(gòu)造函數(shù)的指針,而實(shí)例都包含一個指向原型對象的內(nèi)部指針。那么,假如我們讓原型對象等于另一個類型的實(shí)例,結(jié)果會怎么樣呢?顯然,此時的原型對象將包含一個指向另一個原型的指針,相應(yīng)地,另一個原型中也包含著一個指向另一個構(gòu)造函數(shù)的指針。假如另一個原型又是另一個類型的實(shí)例,那么上述關(guān)系依然成立,如此層層遞進(jìn),就構(gòu)成了實(shí)例與原型的鏈條。這就是所謂原型鏈的基本概念。
實(shí)現(xiàn)原型鏈有一種基本模式,其代碼大致如下。
function SuperType(){
this.property = true;
}
SuperType.prototype.getSuperValue = function(){
return this.property;
};
function SubType(){
this.subproperty = false;
}
//繼承了 SuperType
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function (){
return this.subproperty;
};
var instance = new SubType();
alert(instance.getSuperValue()); //true
解釋:
以上代碼定義了兩個類型: SuperType 和 SubType。每個類型分別有一個屬性和一個方法。它們的主要區(qū)別是 SubType 繼承了 SuperType,而繼承是通過創(chuàng)建 SuperType 的實(shí)例,并將該實(shí)例賦給
SubType.prototype 實(shí)現(xiàn)的。實(shí)現(xiàn)的本質(zhì)是重寫原型對象,代之以一個新類型的實(shí)例。換句話說,原來存在于 SuperType 的實(shí)例中的所有屬性和方法,現(xiàn)在也存在于 SubType.prototype 中了。在確立了繼承關(guān)系之后,我們給 SubType.prototype 添加了一個方法,這樣就在繼承了 SuperType 的屬性和方法的基礎(chǔ)上又添加了一個新方法。這個例子中的實(shí)例以及構(gòu)造函數(shù)和原型之間的關(guān)系如圖 6-4 所示。
在上面的代碼中,我們沒有使用 SubType 默認(rèn)提供的原型,而是給它換了一個新原型;這個新原型就是 SuperType 的實(shí)例。于是,新原型不僅具有作為一個 SuperType 的實(shí)例所擁有的全部屬性和方法,而且其內(nèi)部還有一個指針,指向了 SuperType 的原型。最終結(jié)果就是這樣的: instance 指向 SubType的 原 型 , SubType 的 原 型 又 指 向 SuperType 的 原 型 。 getSuperValue() 方 法 仍 然 還 在SuperType.prototype 中,但 property 則位于 SubType.prototype 中。這是因?yàn)?property 是一個實(shí)例屬性,而 getSuperValue()則是一個原型方法。既然 SubType.prototype 現(xiàn)在是 SuperType的實(shí)例,那么 property 當(dāng)然就位于該實(shí)例中了。此外,要注意 instance.constructor 現(xiàn)在指向的是 SuperType,這是因?yàn)樵瓉?SubType.prototype 中的 constructor 被重寫了的緣故。
原型搜索機(jī)制:通過實(shí)現(xiàn)原型鏈,本質(zhì)上擴(kuò)展了本章前面介紹的原型搜索機(jī)制。讀者大概還記得,當(dāng)以讀取模式訪問一個實(shí)例屬性時,首先會在實(shí)例中搜索該屬性。如果沒有找到該屬性,則會繼續(xù)搜索實(shí)例的原型。在通過原型鏈實(shí)現(xiàn)繼承的情況下,搜索過程就得以沿著原型鏈繼續(xù)向上。就拿上面的例子來說,調(diào)用instance.getSuperValue()會經(jīng)歷三個搜索步驟: 1)搜索實(shí)例; 2)搜索 SubType.prototype;3)搜索 SuperType.prototype,最后一步才會找到該方法。在找不到屬性或方法的情況下,搜索過程總是要一環(huán)一環(huán)地前行到原型鏈末端才會停下來。
1. 別忘記默認(rèn)的原型
事實(shí)上,前面例子中展示的原型鏈還少一環(huán)。我們知道,所有引用類型默認(rèn)都繼承了 Object,而這個繼承也是通過原型鏈實(shí)現(xiàn)的。大家要記住,所有函數(shù)的默認(rèn)原型都是 Object 的實(shí)例,因此默認(rèn)原型都會包含一個內(nèi)部指針,指向 Object.prototype。這也正是所有自定義類型都會繼承 toString()、valueOf()等默認(rèn)方法的根本原因。所以,我們說上面例子展示的原型鏈中還應(yīng)該包括另外一個繼承層次。圖 6-5 為我們展示了該例子中完整的原型鏈。
一句話,SubType 繼承了 SuperType,而 SuperType 繼承了 Object。當(dāng)調(diào)用 instance.toString()時,實(shí)際上調(diào)用的是保存在 Object.prototype 中的那個方法。
2. 確定原型和實(shí)例的關(guān)系
可以通過兩種方式來確定原型和實(shí)例之間的關(guān)系。第一種方式是使用instanceof 操作符,只要用這個操作符來測試實(shí)例與原型鏈中出現(xiàn)過的構(gòu)造函數(shù),結(jié)果就會返回 true。以下幾行代碼就說明了這一點(diǎn)。
alert(instance instanceof Object); //true
alert(instance instanceof SuperType); //true
alert(instance instanceof SubType); //true
由于原型鏈的關(guān)系,我們可以說 instance 是 Object、 SuperType 或 SubType 中任何一個類型的實(shí)例。因此,測試這三個構(gòu)造函數(shù)的結(jié)果都返回了 true。
第二種方式是使用 isPrototypeOf()方法。同樣,只要是原型鏈中出現(xiàn)過的原型,都可以說是該原型鏈所派生的實(shí)例的原型,因此 isPrototypeOf()方法也會返回 true,如下所示。
alert(Object.prototype.isPrototypeOf(instance)); //true
alert(SuperType.prototype.isPrototypeOf(instance)); //true
alert(SubType.prototype.isPrototypeOf(instance)); //true
3. 謹(jǐn)慎地定義方法
子類型有時候需要重寫超類型中的某個方法,或者需要添加超類型中不存在的某個方法。但不管怎樣,給原型添加方法的代碼一定要放在替換原型的語句之后。來看下面的例子。
function SuperType(){
this.property = true;
}
SuperType.prototype.getSuperValue = function(){
return this.property;
};
function SubType(){
this.subproperty = false;
}
//繼承了 SuperType
SubType.prototype = new SuperType();
//添加新方法
SubType.prototype.getSubValue = function (){
return this.subproperty;
};
//重寫超類型中的方法
SubType.prototype.getSuperValue = function (){
return false;
};
var instance = new SubType();
alert(instance.getSuperValue()); //false
在以上代碼中,加粗的部分是兩個方法的定義。第一個方法 getSubValue()被添加到了 SubType中。第二個方法 getSuperValue()是原型鏈中已經(jīng)存在的一個方法,但重寫這個方法將會屏蔽原來的那個方法。 換句話說,當(dāng)通過 SubType 的實(shí)例調(diào)用 getSuperValue()時,調(diào)用的就是這個重新定義的方法;但通過 SuperType 的實(shí)例調(diào)用 getSuperValue()時,還會繼續(xù)調(diào)用原來的那個方法。這里要格外注意的是,必須在用 SuperType 的實(shí)例替換原型之后,再定義這兩個方法。還有一點(diǎn)需要提醒讀者,即在通過原型鏈實(shí)現(xiàn)繼承時,不能使用對象字面量創(chuàng)建原型方法。因?yàn)檫@
樣做就會重寫原型鏈,如下面的例子所示。
function SuperType(){
this.property = true;
}
SuperType.prototype.getSuperValue = function(){
return this.property;
};
function SubType(){
this.subproperty = false;
}
//繼承了 SuperType
SubType.prototype = new SuperType();
//使用字面量添加新方法,會導(dǎo)致上一行代碼無效
SubType.prototype = {
getSubValue : function (){
return this.subproperty;
},
someOtherMethod : function (){
return false;
}
};
var instance = new SubType();
alert(instance.getSuperValue()); //error!
以上代碼展示了剛剛把 SuperType 的實(shí)例賦值給原型,緊接著又將原型替換成一個對象字面量而導(dǎo)致的問題。由于現(xiàn)在的原型包含的是一個 Object 的實(shí)例,而非 SuperType 的實(shí)例,因此我們設(shè)想中的原型鏈已經(jīng)被切斷——SubType 和 SuperType 之間已經(jīng)沒有關(guān)系了。
4. 原型鏈的問題
原型鏈雖然很強(qiáng)大,可以用它來實(shí)現(xiàn)繼承,但它也存在一些問題。其中,最主要的問題來自包含引用類型值的原型。想必大家還記得,我們前面介紹過包含引用類型值的原型屬性會被所有實(shí)例共享;而這也正是為什么要在構(gòu)造函數(shù)中,而不是在原型對象中定義屬性的原因。在通過原型來實(shí)現(xiàn)繼承時,原型實(shí)際上會變成另一個類型的實(shí)例。于是,原先的實(shí)例屬性也就順理成章地變成了現(xiàn)在的原型屬性了。下列代碼可以用來說明這個問題。
function SuperType(){
this.colors = ["red", "blue", "green"];
}
function SubType(){
}
//繼承了 SuperType
SubType.prototype = new SuperType();
var instance1 = new SubType();
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"
var instance2 = new SubType();
alert(instance2.colors); //"red,blue,green,black"
問題:
1.這個例子中的 SuperType 構(gòu)造函數(shù)定義了一個 colors 屬性,該屬性包含一個數(shù)組(引用類型值)。SuperType 的每個實(shí)例都會有各自包含自己數(shù)組的 colors 屬性。當(dāng) SubType 通過原型鏈繼承了SuperType 之后, SubType.prototype 就變成了 SuperType 的一個實(shí)例,因此它也擁有了一個它自己的 colors 屬性——就跟專門創(chuàng)建了一個SubType.prototype.colors 屬性一樣。但結(jié)果是什么呢?結(jié)果是 SubType 的所有實(shí)例都會共享這一個 colors 屬性。 而我們對 instance1.colors 的修改
能夠通過 instance2.colors 反映出來,就已經(jīng)充分證實(shí)了這一點(diǎn)。
2.原型鏈的第二個問題是:在創(chuàng)建子類型的實(shí)例時,不能向超類型的構(gòu)造函數(shù)中傳遞參數(shù)。實(shí)際上,應(yīng)該說是沒有辦法在不影響所有對象實(shí)例的情況下,給超類型的構(gòu)造函數(shù)傳遞參數(shù)。有鑒于此,再加上前面剛剛討論過的由于原型中包含引用類型值所帶來的問題,實(shí)踐中很少會單獨(dú)使用原型鏈。
由于此問題,所以實(shí)踐中很少使用原型鏈。
方式二:借用構(gòu)造函數(shù)
在解決原型中包含引用類型值所帶來問題的過程中,開發(fā)人員開始使用一種叫做借用構(gòu)造函數(shù)(constructor stealing)的技術(shù)(有時候也叫做偽造對象或經(jīng)典繼承)。這種技術(shù)的基本思想相當(dāng)簡單,即在子類型構(gòu)造函數(shù)的內(nèi)部調(diào)用超類型構(gòu)造函數(shù)。別忘了,函數(shù)只不過是在特定環(huán)境中執(zhí)行代碼的對象,因此通過使用 apply()和 call()方法也可以在(將來)新創(chuàng)建的對象上執(zhí)行構(gòu)造函數(shù),如下所示:
function SuperType(){
this.colors = ["red", "blue", "green"];
}
function SubType(){
//繼承了 SuperType
SuperType.call(this);
}
var instance1 = new SubType();
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"
var instance2 = new SubType();
alert(instance2.colors); //"red,blue,green"
代碼中加粗的那一行代碼“借調(diào)”了超類型的構(gòu)造函數(shù)。通過使用 call()方法(或 apply()方法也可以),我們實(shí)際上是在(未來將要)新創(chuàng)建的 SubType 實(shí)例的環(huán)境下調(diào)用了 SuperType 構(gòu)造函數(shù)。這樣一來,就會在新 SubType 對象上執(zhí)行 SuperType()函數(shù)中定義的所有對象初始化代碼。結(jié)果,SubType 的每個實(shí)例就都會具有自己的 colors 屬性的副本了。
1. 傳遞參數(shù)
相對于原型鏈而言,借用構(gòu)造函數(shù)有一個很大的優(yōu)勢,即可以在子類型構(gòu)造函數(shù)中向超類型構(gòu)造函數(shù)傳遞參數(shù)。看下面這個例子。
function SuperType(name){
this.name = name;
}
function SubType(){
//繼承了 SuperType,同時還傳遞了參數(shù)
SuperType.call(this, "Nicholas");
//實(shí)例屬性
this.age = 29;
}
var instance = new SubType();
alert(instance.name); //"Nicholas";
alert(instance.age); //29
以上代碼中的 SuperType 只接受一個參數(shù) name,該參數(shù)會直接賦給一個屬性。在 SubType 構(gòu)造函數(shù)內(nèi)部調(diào)用 SuperType 構(gòu)造函數(shù)時,實(shí)際上是為 SubType 的實(shí)例設(shè)置了 name 屬性。為了確保SuperType 構(gòu)造函數(shù)不會重寫子類型的屬性,可以在調(diào)用超類型構(gòu)造函數(shù)后,再添加應(yīng)該在子類型中定義的屬性。
2. 借用構(gòu)造函數(shù)的問題
如果僅僅是借用構(gòu)造函數(shù),那么也將無法避免構(gòu)造函數(shù)模式存在的問題——方法都在構(gòu)造函數(shù)中定義,因此函數(shù)復(fù)用就無從談起了。而且,在超類型的原型中定義的方法,對子類型而言也是不可見的,結(jié)果所有類型都只能使用構(gòu)造函數(shù)模式。考慮到這些問題,借用構(gòu)造函數(shù)的技術(shù)也是很少單獨(dú)使用的。
方式三:(組合繼承)
組合繼承(combination inheritance),有時候也叫做偽經(jīng)典繼承,指的是將原型鏈和借用構(gòu)造函數(shù)的技術(shù)組合到一塊,從而發(fā)揮二者之長的一種繼承模式。其背后的思路是使用原型鏈實(shí)現(xiàn)對原型屬性和方法的繼承,而通過借用構(gòu)造函數(shù)來實(shí)現(xiàn)對實(shí)例屬性的繼承。這樣,既通過在原型上定義方法實(shí)現(xiàn)了函數(shù)復(fù)用,又能夠保證每個實(shí)例都有它自己的屬性。下面來看一個例子。
function SuperType(name){
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
alert(this.name);
};
function SubType(name, age){
//繼承屬性
SuperType.call(this, name);
this.age = age;
}
//繼承方法
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
alert(this.age);
};
var instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"
instance1.sayName(); //"Nicholas";
instance1.sayAge(); //29
var instance2 = new SubType("Greg", 27);
alert(instance2.colors); //"red,blue,green"
instance2.sayName(); //"Greg";
instance2.sayAge(); //27
解釋:
在這個例子中, SuperType 構(gòu)造函數(shù)定義了兩個屬性: name 和 colors。 SuperType 的原型定義了一個方法 sayName()。 SubType 構(gòu)造函數(shù)在調(diào)用 SuperType 構(gòu)造函數(shù)時傳入了 name 參數(shù),緊接著又定義了它自己的屬性 age。然后,將 SuperType 的實(shí)例賦值給 SubType 的原型,然后又在該新原型上定義了方法 sayAge()。這樣一來,就可以讓兩個不同的 SubType 實(shí)例既分別擁有自己屬性——包括 colors 屬性,又可以使用相同的方法了。
優(yōu)點(diǎn):
組合繼承避免了原型鏈和借用構(gòu)造函數(shù)的缺陷,融合了它們的優(yōu)點(diǎn),成為 JavaScript 中最常用的繼承模式。而且, instanceof 和 isPrototypeOf()也能夠用于識別基于組合繼承創(chuàng)建的對象。
方式四:(原型式繼承)
道格拉斯·克羅克福德在 2006 年寫了一篇文章,題為 Prototypal Inheritance in JavaScript (JavaScript中的原型式繼承)。在這篇文章中,他介紹了一種實(shí)現(xiàn)繼承的方法,這種方法并沒有使用嚴(yán)格意義上的構(gòu)造函數(shù)。他的想法是借助原型可以基于已有的對象創(chuàng)建新對象,同時還不必因此創(chuàng)建自定義類型。為了達(dá)到這個目的,他給出了如下函數(shù)。
function object(o){
function F(){}
F.prototype = o;
return new F();
}
在 object()函數(shù)內(nèi)部,先創(chuàng)建了一個臨時性的構(gòu)造函數(shù),然后將傳入的對象作為這個構(gòu)造函數(shù)的原型,最后返回了這個臨時類型的一個新實(shí)例。從本質(zhì)上講, object()對傳入其中的對象執(zhí)行了一次淺復(fù)制。來看下面的例子。
var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
var yetAnotherPerson = object(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
alert(person.friends); //"Shelby,Court,Van,Rob,Barbie"
克羅克福德主張的這種原型式繼承,要求你必須有一個對象可以作為另一個對象的基礎(chǔ)。如果有這么一個對象的話,可以把它傳遞給 object()函數(shù),然后再根據(jù)具體需求對得到的對象加以修改即可。在這個例子中,可以作為另一個對象基礎(chǔ)的是 person 對象,于是我們把它傳入到 object()函數(shù)中,然后該函數(shù)就會返回一個新對象。這個新對象將 person 作為原型,所以它的原型中就包含一個基本類型值屬性
和一個引用類型值屬性。這意味著 person.friends 不僅屬于 person 所有,而且也會被 anotherPerson以及 yetAnotherPerson 共享。實(shí)際上,這就相當(dāng)于又創(chuàng)建了 person 對象的兩個副本。
ECMAScript 5 通過新增 Object.create()方法規(guī)范化了原型式繼承。這個方法接收兩個參數(shù):一個用作新對象原型的對象和(可選的)一個為新對象定義額外屬性的對象。在傳入一個參數(shù)的情況下,Object.create()與 object()方法的行為相同。
var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = Object.create(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
var yetAnotherPerson = Object.create(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
alert(person.friends); //"Shelby,Court,Van,Rob,Barbie"
Object.create()方法的第二個參數(shù)與Object.defineProperties()方法的第二個參數(shù)格式相同:每個屬性都是通過自己的描述符定義的。以這種方式指定的任何屬性都會覆蓋原型對象上的同名屬性。例如:
var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
{
name: {
value: "Greg"
}
});
alert(anotherPerson.name); //"Greg"
支持版本:支持 Object.create()方法的瀏覽器有 IE9+、 Firefox 4+、 Safari 5+、 Opera 12+和 Chrome。在沒有必要興師動眾地創(chuàng)建構(gòu)造函數(shù),而只想讓一個對象與另一個對象保持類似的情況下,原型式繼承是完全可以勝任的。缺點(diǎn):不過別忘了,包含引用類型值的屬性始終都會共享相應(yīng)的值,就像使用原型模式一樣。
方式五:(寄生式繼承)
寄生式(parasitic)繼承是與原型式繼承緊密相關(guān)的一種思路,并且同樣也是由克羅克福德推而廣之的。寄生式繼承的思路與寄生構(gòu)造函數(shù)和工廠模式類似,即創(chuàng)建一個僅用于封裝繼承過程的函數(shù),該函數(shù)在內(nèi)部以某種方式來增強(qiáng)對象,最后再像真地是它做了所有工作一樣返回對象。以下代碼示范了寄生式繼承模式。
function createAnother(original){
var clone = object(original); //通過調(diào)用函數(shù)創(chuàng)建一個新對象
clone.sayHi = function(){ //以某種方式來增強(qiáng)這個對象
alert("hi");
};
return clone; //返回這個對象
}
在這個例子中, createAnother()函數(shù)接收了一個參數(shù),也就是將要作為新對象基礎(chǔ)的對象。然后,把這個對象(original)傳遞給 object()函數(shù),將返回的結(jié)果賦值給 clone。再為 clone 對象添加一個新方法 sayHi(),最后返回 clone 對象。可以像下面這樣來使用 createAnother()函數(shù):
var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); //"hi"
這個例子中的代碼基于 person 返回了一個新對象——anotherPerson。新對象不僅具有 person的所有屬性和方法,而且還有自己的 sayHi()方法。
使用范圍:在主要考慮對象而不是自定義類型和構(gòu)造函數(shù)的情況下,寄生式繼承也是一種有用的模式。前面示范繼承模式時使用的 object()函數(shù)不是必需的;任何能夠返回新對象的函數(shù)都適用于此模式。
注意:使用寄生式繼承來為對象添加函數(shù),會由于不能做到函數(shù)復(fù)用而降低效率;這一點(diǎn)與構(gòu)造函數(shù)模式類似。
方式六:(寄生組合式繼承)
前面說過,組合繼承是 JavaScript 最常用的繼承模式;不過,它也有自己的不足。組合繼承最大的問題就是無論什么情況下,都會調(diào)用兩次超類型構(gòu)造函數(shù):一次是在創(chuàng)建子類型原型的時候,另一次是在子類型構(gòu)造函數(shù)內(nèi)部。沒錯,子類型最終會包含超類型對象的全部實(shí)例屬性,但我們不得不在調(diào)用子類型構(gòu)造函數(shù)時重寫這些屬性。再來看一看下面組合繼承的例子。
function SuperType(name){
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
alert(this.name);
};
function SubType(name, age){
SuperType.call(this, name); //第二次調(diào)用 SuperType()
this.age = age;
}
SubType.prototype = new SuperType(); //第一次調(diào)用 SuperType()
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
alert(this.age);
};
加粗字體的行中是調(diào)用 SuperType 構(gòu)造函數(shù)的代碼。在第一次調(diào)用 SuperType 構(gòu)造函數(shù)時,SubType.prototype 會得到兩個屬性: name 和 colors;它們都是 SuperType 的實(shí)例屬性,只不過現(xiàn)在位于 SubType 的原型中。當(dāng)調(diào)用 SubType 構(gòu)造函數(shù)時,又會調(diào)用一次 SuperType 構(gòu)造函數(shù),這一次又在新對象上創(chuàng)建了實(shí)例屬性 name 和 colors。于是,這兩個屬性就屏蔽了原型中的兩個同名屬性。圖 6-6 展示了上述過程。
如圖 6-6 所示,有兩組 name 和 colors 屬性:一組在實(shí)例上,一組在 SubType 原型中。這就是調(diào)用兩次 SuperType 構(gòu)造函數(shù)的結(jié)果。好在我們已經(jīng)找到了解決這個問題方法——寄生組合式繼承。
所謂寄生組合式繼承,即通過借用構(gòu)造函數(shù)來繼承屬性,通過原型鏈的混成形式來繼承方法。其背后的基本思路是:不必為了指定子類型的原型而調(diào)用超類型的構(gòu)造函數(shù),我們所需要的無非就是超類型原型的一個副本而已。本質(zhì)上,就是使用寄生式繼承來繼承超類型的原型,然后再將結(jié)果指定給子類型的原型。寄生組合式繼承的基本模式如下所示。
function inheritPrototype(subType, superType){
var prototype = object(superType.prototype); //創(chuàng)建對象
prototype.constructor = subType; //增強(qiáng)對象
subType.prototype = prototype; //指定對象
}
這個示例中的 inheritPrototype()函數(shù)實(shí)現(xiàn)了寄生組合式繼承的最簡單形式。這個函數(shù)接收兩個參數(shù):子類型構(gòu)造函數(shù)和超類型構(gòu)造函數(shù)。在函數(shù)內(nèi)部,第一步是創(chuàng)建超類型原型的一個副本。第二步是為創(chuàng)建的副本添加 constructor 屬性,從而彌補(bǔ)因重寫原型而失去的默認(rèn)的 constructor 屬性。最后一步,將新創(chuàng)建的對象(即副本)賦值給子類型的原型。這樣,我們就可以用調(diào)用 inheritPrototype()函數(shù)的語句,
去替換前面例子中為子類型原型賦值的語句了,例如:
function inheritPrototype(subType, superType){
var prototype = object(superType.prototype); //創(chuàng)建對象
prototype.constructor = subType; //增強(qiáng)對象
subType.prototype = prototype; //指定對象
}
function SuperType(name){
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
alert(this.name);
};
function SubType(name, age){
SuperType.call(this, name);
this.age = age;
}
inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function(){
alert(this.age);
};
這個例子的高效率體現(xiàn)在它只調(diào)用了一次 SuperType 構(gòu)造函數(shù),并且因此避免了在 SubType.prototype 上面創(chuàng)建不必要的、多余的屬性。與此同時,原型鏈還能保持不變;因此,還能夠正常使用instanceof 和 isPrototypeOf()。開發(fā)人員普遍認(rèn)為寄生組合式繼承是引用類型最理想的繼承范式。
首次應(yīng)該次繼承js庫: YUI 的 YAHOO.lang.extend()方法采用了寄生組合繼承,從而讓這種模式首次出現(xiàn)在了一個應(yīng)用非常廣泛的 JavaScript 庫中。要了解有關(guān) YUI 的更多信息,請訪問http://developer. yahoo.com/yui/。
總結(jié):
ECMAScript 支持面向?qū)ο螅∣O)編程,但不使用類或者接口。對象可以在代碼執(zhí)行過程中創(chuàng)建和增強(qiáng),因此具有動態(tài)性而非嚴(yán)格定義的實(shí)體。在沒有類的情況下,可以采用下列模式創(chuàng)建對象。
? 工廠模式,使用簡單的函數(shù)創(chuàng)建對象,為對象添加屬性和方法,然后返回對象。這個模式后來被構(gòu)造函數(shù)模式所取代。
? 構(gòu)造函數(shù)模式,可以創(chuàng)建自定義引用類型,可以像創(chuàng)建內(nèi)置對象實(shí)例一樣使用 new 操作符。不過,構(gòu)造函數(shù)模式也有缺點(diǎn),即它的每個成員都無法得到復(fù)用,包括函數(shù)。由于函數(shù)可以不局限于任何對象(即與對象具有松散耦合的特點(diǎn)),因此沒有理由不在多個對象間共享函數(shù)。
? 原型模式,使用構(gòu)造函數(shù)的 prototype 屬性來指定那些應(yīng)該共享的屬性和方法。組合使用構(gòu)造函數(shù)模式和原型模式時,使用構(gòu)造函數(shù)定義實(shí)例屬性,而使用原型定義共享的屬性和方法。
JavaScript 主要通過原型鏈實(shí)現(xiàn)繼承。原型鏈的構(gòu)建是通過將一個類型的實(shí)例賦值給另一個構(gòu)造函數(shù)的原型實(shí)現(xiàn)的。這樣,子類型就能夠訪問超類型的所有屬性和方法,這一點(diǎn)與基于類的繼承很相似。原型鏈的問題是對象實(shí)例共享所有繼承的屬性和方法,因此不適宜單獨(dú)使用。解決這個問題的技術(shù)是借用構(gòu)造函數(shù),即在子類型構(gòu)造函數(shù)的內(nèi)部調(diào)用超類型構(gòu)造函數(shù)。這樣就可以做到每個實(shí)例都具有自己的屬性,同時還能保證只使用構(gòu)造函數(shù)模式來定義類型。使用最多的繼承模式是組合繼承,這種模式使用原型鏈繼承共享的屬性和方法,而通過借用構(gòu)造函數(shù)繼承實(shí)例屬性。
此外,還存在下列可供選擇的繼承模式。
1.? 原型式繼承,可以在不必預(yù)先定義構(gòu)造函數(shù)的情況下實(shí)現(xiàn)繼承,其本質(zhì)是執(zhí)行對給定對象的淺復(fù)制。而復(fù)制得到的副本還可以得到進(jìn)一步改造。
2.? 寄生式繼承,與原型式繼承非常相似,也是基于某個對象或某些信息創(chuàng)建一個對象,然后增強(qiáng)對象,最后返回對象。為了解決組合繼承模式由于多次調(diào)用超類型構(gòu)造函數(shù)而導(dǎo)致的低效率問題,可以將這個模式與組合繼承一起使用。
3.? 寄生組合式繼承,集寄生式繼承和組合繼承的優(yōu)點(diǎn)與一身,是實(shí)現(xiàn)基于類型繼承的最有效方式。