繼承是 OO 語言中的一個最為人津津樂道的概念。許多 OO 語言都支持兩種繼承方式:接口繼承 和 實現(xiàn)繼承。接口繼承只繼承方法簽名,而實現(xiàn)繼承則繼承實際的方法。
如前所述,由于函數(shù)沒有簽名,在 ECMAScript 中無法實現(xiàn)接口繼承。ECMAScript 只支持實現(xiàn)繼承,而且其實現(xiàn)繼承主要是依靠原型鏈來實現(xiàn)的。
一、原型鏈
ECMAScript 中描述了原型鏈的概念,并將原型鏈作為實現(xiàn)繼承的主要方法。其基本思想是利用原型讓一個引用類型繼承另一個引用類型的屬性和方法。
簡單回顧一下構造函數(shù)、原型和實例的關系:每個構造函數(shù)都有一個原型對象,原型對象都包含一個指向構造函數(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();
console.log(instance.getSuperValue()); // true
以上代碼定義了兩個類型:SuperType 和 SubType。每個類型分別有一個屬性和一個方法。它們的主要區(qū)別是 SubType 繼承了 SuperType,而繼承是通過創(chuàng)建 SuperType 的實例,并將該實例賦給SubType.prototype 實現(xiàn)的。
實現(xiàn)的本質是重寫原型對象,代之以一個新類型的實例。換句話說,原來存在于 SuperType 的實例中的所有屬性和方法,現(xiàn)在也存在于 SubType.prototype 中了。在確立了繼承關系之后,我們給 SubType.prototype 添加了一個方法,這樣就在繼承了 SuperType 的屬性和方法的基礎上又添加了一個新方法。
這個例子中的實例以及構造函數(shù)和原型之間的關系如下所示。

在上面的代碼中,我們沒有使用 SubType 默認提供的原型,而是給它換了一個新原型,這個新原型就是 SuperType 的實例。
于是,新原型不僅具有作為一個 SuperType 的實例所擁有的全部屬性和方法,而且其內部還有一個指針,指向了 SuperType 的原型。
最終結果就是這樣的:instance 指向 SubType 的原型, SubType 的原型又指向 SuperType 的原型。
getSuperValue() 方法仍然還在SuperType.prototype 中,但 property 則位于 SubType.prototype 中。這是因為 property 是一個實例屬性,而 getSuperValue() 則是一個原型方法。既然 SubType.prototype 現(xiàn)在是 SuperType 的實例,那么 property 當然就位于該實例中了。此外,要注意 instance.constructor 現(xiàn)在指向的是 SuperType,這是因為原來 SubType.prototype 中的 constructor 被重寫了的緣故。
通過實現(xiàn)原型鏈,本質上擴展了原型搜索機制。當以讀取模式訪問一個實例屬性時,首先會在實例中搜索該屬性。如果沒有找到該屬性,則會繼續(xù)搜索實例的原型。在通過原型鏈實現(xiàn)繼承的情況下,搜索過程就得以沿著原型鏈繼續(xù)向上。
就拿上面的例子來說,調用 instance.getSuperValue() 會經(jīng)歷三個搜索步驟:
- 搜索實例;
- 搜索
SubType.prototype; - 搜索
SuperType.prototype,最后一步才會找到該方法。
在找不到屬性或方法的情況下,搜索過程總是要一環(huán)一環(huán)地前行到原型鏈末端才會停下來。
1. 別忘記默認的原型
事實上,前面例子中展示的原型鏈還少一環(huán)。我們知道,所有引用類型默認都繼承了 Object,而這個繼承也是通過原型鏈實現(xiàn)的。
大家要記住,所有函數(shù)的默認原型都是 Object 的實例,因此默認原型都會包含一個內部指針,指向Object.prototype。這也正是所有自定義類型都會繼承 toString()、valueOf() 等默認方法的根本原因。所以,我們說上面例子展示的原型鏈中還應該包括另外一個繼承層次。下圖為我們展示了該例子中完整的原型鏈。

一句話,
SubType 繼承了 SuperType,而 SuperType 繼承了 Object。當調用 instance.toString() 時,實際上調用的是保存在 Object.prototype 中的那個方法。
2. 確定原型和實例的關系
可以通過兩種方式來確定原型和實例之間的關系。
- 第一種方式是使用
instanceof操作符,只要用這個操作符來測試實例與原型鏈中出現(xiàn)過的構造函數(shù),結果就會返回true。以下幾行代碼就說明了這一點。
console.log(instance instanceof Object); // true
console.log(instance instanceof SuperType); // true
console.log(instance instanceof SubType); // true
由于原型鏈的關系,我們可以說 instance 是 Object、SuperType 或 SubType 中任何一個類型的實例。因此,測試這三個構造函數(shù)的結果都返回了 true。
- 第二種方式是使用
isPrototypeOf()方法。同樣,只要是原型鏈中出現(xiàn)過的原型,都可以說是該原型鏈所派生的實例的原型,因此isPrototypeOf()方法也會返回true,如下所示。
console.log(Object.prototype.isPrototypeOf(instance)); // true
console.log(SuperType.prototype.isPrototypeOf(instance)); // true
console.log(SubType.prototype.isPrototypeOf(instance)); // true
3. 謹慎地定義方法
子類型有時候需要重寫超類型中的某個方法,或者需要添加超類型中不存在的某個方法。但不管怎樣,給原型添加方法的代碼一定要放在替換原型的語句之后。來看下面的例子。
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();
console.log(instance.getSuperValue()); // false
在以上代碼中,加粗的部分是兩個方法的定義。第一個方法 getSubValue() 被添加到了 SubType中。第二個方法 getSuperValue() 是原型鏈中已經(jīng)存在的一個方法,但重寫這個方法將會屏蔽原來的那個方法。換句話說,當通過 SubType 的實例調用 getSuperValue() 時,調用的就是這個重新定義的方法;但通過 SuperType 的實例調用 getSuperValue() 時,還會繼續(xù)調用原來的那個方法。這里要格外注意的是,必須在用 SuperType 的實例替換原型之后,再定義這兩個方法。
還有一點需要提醒讀者,即在通過原型鏈實現(xiàn)繼承時,不能使用對象字面量創(chuàng)建原型方法。因為這樣做就會重寫原型鏈,如下面的例子所示。
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;
},
someOtherMethod : function (){
return false;
}
};
var instance = new SubType();
console.log(instance.getSuperValue()); // error!
以上代碼展示了剛剛把 SuperType 的實例賦值給原型,緊接著又將原型替換成一個對象字面量而導致的問題。由于現(xiàn)在的原型包含的是一個 Object 的實例,而非 SuperType 的實例,因此我們設想中的原型鏈已經(jīng)被切斷——SubType 和 SuperType 之間已經(jīng)沒有關系了。
4. 原型鏈的問題
原型鏈雖然很強大,可以用它來實現(xiàn)繼承,但它也存在一些問題。其中,最主要的問題來自包含引用類型值的原型。想必大家還記得,我們前面介紹過包含引用類型值的原型屬性會被所有實例共享;而這也正是為什么要在構造函數(shù)中,而不是在原型對象中定義屬性的原因。在通過原型來實現(xiàn)繼承時,原型實際上會變成另一個類型的實例。于是,原先的實例屬性也就順理成章地變成了現(xiàn)在的原型屬性了。
下列代碼可以用來說明這個問題。
function SuperType(){
this.colors = ["red", "blue", "green"];
}
function SubType(){
}
//繼承了 SuperType
SubType.prototype = new SuperType();
var instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors); //"red,blue,green,black"
var instance2 = new SubType();
console.log(instance2.colors); //"red,blue,green,black"
這個例子中的 SuperType 構造函數(shù)定義了一個 colors 屬性,該屬性包含一個數(shù)組(引用類型值)。SuperType 的每個實例都會有各自包含自己數(shù)組的 colors 屬性。當 SubType 通過原型鏈繼承了SuperType 之后,SubType.prototype 就變成了 SuperType 的一個實例,因此它也擁有了一個它自己的 colors 屬性——就跟專門創(chuàng)建了一個 SubType.prototype.colors 屬性一樣。但結果是什么呢?結果是 SubType 的所有實例都會共享這一個 colors 屬性。而我們對 instance1.colors 的修改能夠通過 instance2.colors 反映出來,就已經(jīng)充分證實了這一點。
原型鏈的第二個問題是:在創(chuàng)建子類型的實例時,不能向超類型的構造函數(shù)中傳遞參數(shù)。實際上,應該說是沒有辦法在不影響所有對象實例的情況下,給超類型的構造函數(shù)傳遞參數(shù)。有鑒于此,再加上前面剛剛討論過的由于原型中包含引用類型值所帶來的問題,實踐中很少會單獨使用原型鏈。
二、借用構造函數(shù)
在解決原型中包含引用類型值所帶來問題的過程中,開發(fā)人員開始使用一種叫做借用構造函數(shù)(constructor stealing)的技術(有時候也叫做偽造對象或經(jīng)典繼承)。這種技術的基本思想相當簡單,即在子類型構造函數(shù)的內部調用超類型構造函數(shù)。別忘了,函數(shù)只不過是在特定環(huán)境中執(zhí)行代碼的對象,因此通過使用 apply()和 call()方法也可以在(將來)新創(chuàng)建的對象上執(zhí)行構造函數(shù),如下所示:
function SuperType(){
this.colors = ["red", "blue", "green"];
}
function SubType(){
//繼承了 SuperType
SuperType.call(this);
}
var instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors); //"red,blue,green,black"
var instance2 = new SubType();
console.log(instance2.colors); //"red,blue,green"
代碼中加粗的那一行代碼“借調”了超類型的構造函數(shù)。通過使用 call()方法(或 apply()方法也可以),我們實際上是在(未來將要)新創(chuàng)建的 SubType 實例的環(huán)境下調用了 SuperType 構造函數(shù)。
這樣一來,就會在新 SubType 對象上執(zhí)行 SuperType()函數(shù)中定義的所有對象初始化代碼。結果,SubType 的每個實例就都會具有自己的 colors 屬性的副本了。
1. 傳遞參數(shù)
相對于原型鏈而言,借用構造函數(shù)有一個很大的優(yōu)勢,即可以在子類型構造函數(shù)中向超類型構造函數(shù)傳遞參數(shù)??聪旅孢@個例子。
function SuperType(name){
this.name = name;
}
function SubType(){
//繼承了 SuperType,同時還傳遞了參數(shù)
SuperType.call(this, "Nicholas");
//實例屬性
this.age = 29;
}
var instance = new SubType();
console.log(instance.name); //"Nicholas";
console.log(instance.age); //29
以上代碼中的 SuperType 只接受一個參數(shù) name,該參數(shù)會直接賦給一個屬性。在 SubType 構造函數(shù)內部調用 SuperType 構造函數(shù)時,實際上是為 SubType 的實例設置了 name 屬性。為了確保SuperType 構造函數(shù)不會重寫子類型的屬性,可以在調用超類型構造函數(shù)后,再添加應該在子類型中定義的屬性。
2. 借用構造函數(shù)的問題
如果僅僅是借用構造函數(shù),那么也將無法避免構造函數(shù)模式存在的問題——方法都在構造函數(shù)中定義,因此函數(shù)復用就無從談起了。而且,在超類型的原型中定義的方法,對子類型而言也是不可見的,結果所有類型都只能使用構造函數(shù)模式。考慮到這些問題,借用構造函數(shù)的技術也是很少單獨使用的。
三、組合繼承
組合繼承(combination inheritance),有時候也叫做偽經(jīng)典繼承,指的是將原型鏈和借用構造函數(shù)的技術組合到一塊,從而發(fā)揮二者之長的一種繼承模式。其背后的思路是使用原型鏈實現(xiàn)對原型屬性和方法的繼承,而通過借用構造函數(shù)來實現(xiàn)對實例屬性的繼承。這樣,既通過在原型上定義方法實現(xiàn)了函數(shù)復用,又能夠保證每個實例都有它自己的屬性。下面來看一個例子。
function SuperType(name){
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
console.log(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(){
console.log(this.age);
};
var instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
console.log(instance1.colors); //"red,blue,green,black"
instance1.sayName(); //"Nicholas";
instance1.sayAge(); //29
var instance2 = new SubType("Greg", 27);
console.log(instance2.colors); //"red,blue,green"
instance2.sayName(); //"Greg";
instance2.sayAge(); //27
在這個例子中,SuperType 構造函數(shù)定義了兩個屬性:name 和 colors。SuperType 的原型定義了一個方法 sayName()。SubType 構造函數(shù)在調用 SuperType 構造函數(shù)時傳入了 name 參數(shù),緊接著又定義了它自己的屬性 age。然后,將 SuperType 的實例賦值給 SubType 的原型,然后又在該新原型上定義了方法 sayAge()。這樣一來,就可以讓兩個不同的 SubType 實例既分別擁有自己屬性——包括 colors 屬性,又可以使用相同的方法了。
組合繼承避免了原型鏈和借用構造函數(shù)的缺陷,融合了它們的優(yōu)點,成為 JavaScript 中最常用的繼承模式。而且,instanceof 和 isPrototypeOf()也能夠用于識別基于組合繼承創(chuàng)建的對象。
四、原型式繼承
道格拉斯·克羅克福德在 2006 年寫了一篇文章,題為 Prototypal Inheritance in JavaScript (JavaScript 中的原型式繼承)。在這篇文章中,他介紹了一種實現(xiàn)繼承的方法,這種方法并沒有使用嚴格意義上的構造函數(shù)。他的想法是借助原型可以基于已有的對象創(chuàng)建新對象,同時還不必因此創(chuàng)建自定義類型。為了達到這個目的,他給出了如下函數(shù)。
function object(o){
function F(){}
F.prototype = o;
return new F();
}
在 object()函數(shù)內部,先創(chuàng)建了一個臨時性的構造函數(shù),然后將傳入的對象作為這個構造函數(shù)的原型,最后返回了這個臨時類型的一個新實例。從本質上講,object()對傳入其中的對象執(zhí)行了一次淺復制。來看下面的例子。
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");
console.log(person.friends); // "Shelby,Court,Van,Rob,Barbie"
克羅克福德主張的這種原型式繼承,要求你必須有一個對象可以作為另一個對象的基礎。如果有這么一個對象的話,可以把它傳遞給 object()函數(shù),然后再根據(jù)具體需求對得到的對象加以修改即可。在這個例子中,可以作為另一個對象基礎的是 person 對象,于是我們把它傳入到 object()函數(shù)中,然后該函數(shù)就會返回一個新對象。這個新對象將 person 作為原型,所以它的原型中就包含一個基本類型值屬性和一個引用類型值屬性。這意味著 person.friends 不僅屬于 person 所有,而且也會被 anotherPerson以及 yetAnotherPerson 共享。實際上,這就相當于又創(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");
console.log(person.friends); //"Shelby,Court,Van,Rob,Barbie"
Object.create()方法的第二個參數(shù)與Object.defineProperties()方法的第二個參數(shù)格式相同:每個屬性都是通過自己的描述符定義的。以這種方式指定的任何屬性都會覆蓋原型對象上的同名屬性。例如:
var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = Object.create(person, {
name: {
value: "Greg"
}
});
console.log(anotherPerson.name); // "Greg"
支持 Object.create()方法的瀏覽器有 IE9+、Firefox 4+、Safari 5+、Opera 12+和 Chrome。
在沒有必要興師動眾地創(chuàng)建構造函數(shù),而只想讓一個對象與另一個對象保持類似的情況下,原型式繼承是完全可以勝任的。不過別忘了,包含引用類型值的屬性始終都會共享相應的值,就像使用原型模式一樣。
五、寄生式繼承
寄生式(parasitic)繼承是與原型式繼承緊密相關的一種思路,并且同樣也是由克羅克福德推而廣之的。寄生式繼承的思路與寄生構造函數(shù)和工廠模式類似,即創(chuàng)建一個僅用于封裝繼承過程的函數(shù),該函數(shù)在內部以某種方式來增強對象,最后再像真地是它做了所有工作一樣返回對象。以下代碼示范了寄生式繼承模式。
function createAnother(original){
var clone = object(original); //通過調用函數(shù)創(chuàng)建一個新對象
clone.sayHi = function(){ //以某種方式來增強這個對象
alert("hi");
};
return clone; //返回這個對象
}
在這個例子中,createAnother()函數(shù)接收了一個參數(shù),也就是將要作為新對象基礎的對象。然后,把這個對象(original)傳遞給 object()函數(shù),將返回的結果賦值給 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()方法。
在主要考慮對象而不是自定義類型和構造函數(shù)的情況下,寄生式繼承也是一種有用的模式。前面示范繼承模式時使用的 object()函數(shù)不是必需的;任何能夠返回新對象的函數(shù)都適用于此模式。
使用寄生式繼承來為對象添加函數(shù),會由于不能做到函數(shù)復用而降低效率;這一點與構造函數(shù)模式類似。
六、寄生組合式繼承
前面說過,組合繼承是 JavaScript 最常用的繼承模式;不過,它也有自己的不足。組合繼承最大的問題就是無論什么情況下,都會調用兩次超類型構造函數(shù):一次是在創(chuàng)建子類型原型的時候,另一次是在子類型構造函數(shù)內部。沒錯,子類型最終會包含超類型對象的全部實例屬性,但我們不得不在調用子類型構造函數(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); //第二次調用 SuperType()
this.age = age;
}
SubType.prototype = new SuperType(); //第一次調用 SuperType()
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
alert(this.age);
};
加粗字體的行中是調用 SuperType 構造函數(shù)的代碼。在第一次調用 SuperType 構造函數(shù)時,SubType.prototype 會得到兩個屬性:name 和 colors;它們都是 SuperType 的實例屬性,只不過現(xiàn)在位于 SubType 的原型中。當調用 SubType 構造函數(shù)時,又會調用一次 SuperType 構造函數(shù),這一次又在新對象上創(chuàng)建了實例屬性 name 和 colors。于是,這兩個屬性就屏蔽了原型中的兩個同名屬性。下圖展示了上述過程。

如圖所示,有兩組 name 和 colors 屬性:一組在實例上,一組在 SubType 原型中。這就是調用兩次 SuperType 構造函數(shù)的結果。好在我們已經(jīng)找到了解決這個問題方法——寄生組合式繼承。
所謂寄生組合式繼承,即通過借用構造函數(shù)來繼承屬性,通過原型鏈的混成形式來繼承方法。其背后的基本思路是:不必為了指定子類型的原型而調用超類型的構造函數(shù),我們所需要的無非就是超類型原型的一個副本而已。本質上,就是使用寄生式繼承來繼承超類型的原型,然后再將結果指定給子類型的原型。寄生組合式繼承的基本模式如下所示。
function inheritPrototype(subType, superType){
var prototype = object(superType.prototype); //創(chuàng)建對象
prototype.constructor = subType; //增強對象
subType.prototype = prototype; //指定對象
}
這個示例中的 inheritPrototype()函數(shù)實現(xiàn)了寄生組合式繼承的最簡單形式。這個函數(shù)接收兩個參數(shù):子類型構造函數(shù)和超類型構造函數(shù)。在函數(shù)內部,第一步是創(chuàng)建超類型原型的一個副本。第二步是為創(chuàng)建的副本添加 constructor 屬性,從而彌補因重寫原型而失去的默認的 constructor 屬性。最后一步,將新創(chuàng)建的對象(即副本)賦值給子類型的原型。這樣,我們就可以用調用 inheritPrototype()函數(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;
}
inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function(){
alert(this.age);
};
這個例子的高效率體現(xiàn)在它只調用了一次 SuperType 構造函數(shù),并且因此避免了在 SubType.prototype 上面創(chuàng)建不必要的、多余的屬性。與此同時,原型鏈還能保持不變;因此,還能夠正常使用instanceof 和 isPrototypeOf()。開發(fā)人員普遍認為寄生組合式繼承是引用類型最理想的繼承范式。
總結
ECMAScript 支持面向對象(OO)編程,但不使用類或者接口。對象可以在代碼執(zhí)行過程中創(chuàng)建和增強,因此具有動態(tài)性而非嚴格定義的實體。在沒有類的情況下,可以采用下列模式創(chuàng)建對象。
- 工廠模式,使用簡單的函數(shù)創(chuàng)建對象,為對象添加屬性和方法,然后返回對象。這個模式后來被構造函數(shù)模式所取代。
- 構造函數(shù)模式,可以創(chuàng)建自定義引用類型,可以像創(chuàng)建內置對象實例一樣使用 new 操作符。不過,構造函數(shù)模式也有缺點,即它的每個成員都無法得到復用,包括函數(shù)。由于函數(shù)可以不局限于任何對象(即與對象具有松散耦合的特點),因此沒有理由不在多個對象間共享函數(shù)。
- 原型模式,使用構造函數(shù)的 prototype 屬性來指定那些應該共享的屬性和方法。組合使用構造函數(shù)模式和原型模式時,使用構造函數(shù)定義實例屬性,而使用原型定義共享的屬性和方法。
JavaScript 主要通過原型鏈實現(xiàn)繼承。原型鏈的構建是通過將一個類型的實例賦值給另一個構造函數(shù)的原型實現(xiàn)的。這樣,子類型就能夠訪問超類型的所有屬性和方法,這一點與基于類的繼承很相似。
原型鏈的問題是對象實例共享所有繼承的屬性和方法,因此不適宜單獨使用。解決這個問題的技術是借用構造函數(shù),即在子類型構造函數(shù)的內部調用超類型構造函數(shù)。這樣就可以做到每個實例都具有自己的屬性,同時還能保證只使用構造函數(shù)模式來定義類型。使用最多的繼承模式是組合繼承,這種模式使用原型鏈繼承共享的屬性和方法,而通過借用構造函數(shù)繼承實例屬性。
此外,還存在下列可供選擇的繼承模式。
- 原型式繼承,可以在不必預先定義構造函數(shù)的情況下實現(xiàn)繼承,其本質是執(zhí)行對給定對象的淺復制。而復制得到的副本還可以得到進一步改造。
- 寄生式繼承,與原型式繼承非常相似,也是基于某個對象或某些信息創(chuàng)建一個對象,然后增強對象,最后返回對象。為了解決組合繼承模式由于多次調用超類型構造函數(shù)而導致的低效率問題,可以將這個模式與組合繼承一起使用。
- 寄生組合式繼承,集寄生式繼承和組合繼承的優(yōu)點與一身,是實現(xiàn)基于類型繼承的最有效方式。