內容來自《JavaScript高級程序設計》第三版第6章第3節
原型鏈
ECMAScript中描述了 原型鏈的概念,并將原型鏈作為實現繼承的主要方法。基本思想是利用原型讓一個引用類型繼承另一個引用了類型的屬性和方法。簡單回顧一下構造函數、原型和實例的關系:每個構造函數都有一個原型對象,原型對象包含一個指向構造函數的指針(constructor),而實例都包含一個指向原型對象的內部指針(proto.constructor)。那么,加入我們讓原型對象等于另一個對象的實例,結果會怎么樣呢?顯然,此時的原型對象將寶航一個指向另一個原型的指針,相應地,另一個原型中也包含著一個指向另一個構造函數的指針。假如另一個原型又是另一個原型的實例,那么上述關系依然成立,如此層層遞進,就構成了實例與原型的鏈條。這就是所謂原型鏈的基本概念。
實現原型鏈有一種基本模式:
function SuperType () {
this.prototype = true;
}
SuperType.prototype.getSuperValue = function () {
return this.subprototype;
}
function SubType () {
this.subprototype = false;
}
//繼承了SuperType的實例
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.getSubValue = function(){
return this.subprototype;
};
var instance = new SubType();
console.log(instance.getSuperValue);//true
以上代碼定義了一個類型:SuperType和SubType。每個類型分別有一個屬性和方法。它們的主要區別是SubType繼承了SuperType。而繼承是用過創建SuperType的實例,并將該實例賦值給SubType.prototype實現的。實現的本質是重寫原型對象,代之以一個新類型的實例。換句話說,原來存在于SuperType的實例中的所有屬性和方法,現在也存在于SubType.prototype中了,在確立了繼承關系之后,我們給SubType.prototype添加了一個方法,這樣在繼承了SuperType的屬性和方法的基礎上又添加了一個新方法。
這里我們沒有使用SubType默認提供的原型,而是給它換了一個新的原型;這個新原型就是SuperType的實例。于是,新原型不僅具有作為一個SuperType的實例所擁有的全部屬性和方法,而且其內部還有一個指針,指向了SuperType的原型。最終結果是這樣的:instance的原型指向SubType的原型,SubType的原型又指向SuperType的原型。getSuperValue()方法仍然還在SuperType.prototype中, 但prototype則位于SubType.prototype中。這是因為prototype是一個實例屬性,而getSuperVale()則是一個原型方法。
注意此時instance.constructor指向SuperType。
- 別忘記默認原型
前面的例子展示的原型鏈還少一環。所有引用類型默認繼承了Object,而這個繼承也是通過原型鏈實現的。所有好事的more呢原型都是Object的實例,因此默認原型都會包含一個內部指針,指向Object.prototype。
一句話,SubType繼承了SuperType,二SuperType繼承了Object。
- 確定原型和實例的關系
可以通過兩種方式確定原型和實例之前的關系。
第一種方式是使用instanceof
操作符,只要這個操作符來測試實例與原型鏈中出現過的構造函數,結果就會返回true
。
console.log(instance instanceof Object);//true
console.log(instance instanceof SuperType);//true
console.log(instance instanceof SubType);//true
第二種方式是使用isprototypeOf()
方法。同樣,只要原型鏈中出現過的原型,都可以說是該原型所派生的實例的原型,因此isprototypeOf()
方法也會返回true
。
console.log(Object.prototype.isPrototypeOf(instance));
console.log(SuperType.prototype.isPrototypeOf(instance));
console.log(SubType.prototype.isPrototypeOf(instance));
- 謹慎地定義方法
子類型有時候需要重寫超類型中的某個方法,或者添加超類型中不存在的某個方法。但不管怎樣,給原型添加方法的代碼一定要放在替換原型的語句之后。在通過原型鏈實現繼承時,不能使用對象字面量創建原型方法。因為這楊會重寫原型鏈。
- 原型鏈的問題
最主要的問題來自包含引用類型值的原型。包含引用類型值的原型屬性會被所有實例共享,在通過原型來實現繼承時,原型會變成另一個對象的實例,于是原先實例的屬性也就成了現在的原型屬性了。這樣所有實例都會共享同一個引用類型的原型屬性,修改其中一個的值,其他所有實例的值都會隨之修改。
原型鏈的第二個問題是在創建子類型的實例時,不能向超類型的構造函數中傳遞參數,應該說是沒有辦法在不影響所有對象實例的情況下,給超類型的構造函數傳遞參數。
借用構造函數
在解決原型中包含引用類型值所帶來問題的過程中,開發人員開始使用一種交錯借用構造函數的技術(有時候也叫做偽造對象或經典繼承),即在子類型構造函數的內部調用超類型構造函數,通過使用apply()和call()方法可以在新創建的對象上執行構造函數。
function SuperType () {
this.color = ['red','blue','green'];
}
function SubType () {
// 繼承了SuperType
SuperType.call(this)
}
var instance1 = new SubType();
instance1.color.push('blcak');
console.log(instance1.color); //["red", "blue", "green", "blcak"]
var instance2 = new SubType();
console.log(instance2.color); //["red", "blue", "green"]
通過使用call()
方法(或apply()
方法也可以),我們實際上是在新創建的SubType
實例中調用了SuperType
構造函數。這樣一來,就會在新SubType對象上執行SuperType()函數中定義的所有對象初始化代碼。結果,SubType的每個實例就會都具有自己的colors屬性的副本了。
- 傳遞參數
相對于原型鏈而言,借用構造函數有一個很大的有時,即可用在子類型構造函數中向超類型構造函數中傳遞參數。
function SuperType (name) {
this.name=name;
}
function SubType () {
//繼承了SuperType
SuperType.call(this,'Nicholas');
//實例屬性
this.age=19;
}
var instance = new SubType();
console.log(instance.name);
console.log(instance.age);
- 借用構造函數的問題
如果僅僅是借用構造函數,那么也將無法避免構造函數模式存在的問題————方法都在構造函數中定義,因此函數服用就無從談起了。而且,在超類型的原型中定義的方法,在子類型而言也是不可見的,結果所有類型都只能使用構造函數模式。
組合繼承
組合繼承,有時候也叫做偽經典繼承,指的是將原型鏈和借用構造函數的技術組合到一塊,從而發揮二者之長的一種繼承模式。其背后的思路是使用原型鏈實現對原型屬性和方法的繼承,二通過借用構造函數來實現對實例屬性的繼承。這樣,既通過在原型上定義方法實現了函數復用,又能保證每個實例都有它自己的屬性。
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);
instance1.sayName();
instance1.SayAge();
var instance2 = new SubType('Greg',27);
console.log(instance2.colors);
instance2.sayName();
instance2.SayAge();
在這個例子中,SuperType構造函數定義了兩個屬性:name和colors。SuperType的原型定義了一個方法sayName()。SubType構造函數在調用SuperType構造函數時傳入了name參數,緊接著又定義了它自己的屬性age。然后將SuperType的實例賦值給了SubType的原型,然后又在該新原型上定義了方法sayAge()。這樣一來,就可以讓兩個不同的SubType實例既分別擁有自己的屬性————包括colors屬性,又可以使用相同的方法了。
組合繼承避免了原型鏈和借用構造函數的缺陷,融合了它們的優點,成為JavaScript中最常用的繼承模式。而且,instanceof和isPrototypeOf()也能夠用于識別基于組合繼承創建的對象
原型式繼承
道格拉斯·克羅克福德在2006年鞋了一篇文章,題為Prototypal Inheritance in JavaScript (JavaScript中的原型繼承)。在這片文章中,他介紹了一種實現繼承的方法,這種方法沒有使用嚴格意義上的構造函數。他的想法是借助原型可以基于已有的對象創建新對象,同事還不必因此創建自己定義類型,為了達到這個目的,他給出了如下函數。
function object(o){
function F(){}
F.prototype = o;
return new F();
}
從本質上講,object()對傳入的對象執行了一次淺復制。
這種原型式繼承,要求你筆記有一個對象可以作為另一個對象的原型。ECMAScript5通過新增Object.create()方法規范化了原型式繼承。這個方法接收兩個參數:一個用作新對象原型的對象,一個為新對象定義額外屬性的對象(可選)。在傳入一個參數的情況下,Object.create()與object()方法的行為相同。
支持Object.create()方法的瀏覽器有IE9+、Firefox 4+、Opera 12+ 和Chrome。
寄生式繼承
寄生式繼承是與原型式繼承緊密相關的一種思路,并且同樣也是由克羅克福德推而廣之的。寄生式繼承的思路與寄生構造函數和工廠模式類似,即創建一個僅用于封裝繼承過程的函數,該函數在內部以某種方式來增強對象,最后再想真的是它做了所有工作一樣返回對象。
function createAnother(original){
var clone = object(Original);
clone.sayHi=function(){
console.log('hi');
}
return clone;
}
在這個例子中,createAnother()函數接收了一個參數,也就是將要作為新對象基礎的對象。然后,吧這個對象(original)傳遞給object()函數,將返回的結果賦值給clone。再為clone對象添加一個新方法sayHi(),最后返回clone對象。可以向廈門這樣來使用createAnother()函數。
var person = {
name = "Nicholas",
friends = ['aaa','bbb,'ccc']
}
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); //'hi'
在主要考慮對象而不是自定義類型和構造函數的情況下,寄生式繼承也是一種游泳的模式。前面示范繼承模式時使用的object()函數不是必須的,任何能夠返回新對象的函數都適用于此模式。
寄生組合式繼承
組合式繼承是JavaScript最常用的繼承模式;不過,它也有自己的不足。組合繼承最大的問題就是無論什么情況下,都會調用兩次超類型構造函數:一次是在創建子類型原型的時候,另一次實在子類型構造函數內部。沒錯,子類型最終會包含超類型對象的全部實例屬性,但我們不得不在調用子類型構造函數時重寫這些屬性。
function SuperType(name){
this.name = name;
this.color = ['red','blue','green'];
}
SuperType.prototype.sayName = function(){
console.log(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(){
console.log(this.age);
}
在第一次調用SuperType構造函數,這一次又在新對象上創建了實例屬性name和colors。于是,這兩個屬性就拼比了原型中的兩個同名屬性。 構造函數時,SubType.prototype會得到兩個屬性:name和colors;他們都是SuperType的實例屬性,只不過現在位于SubType的原型中。當調用SubType構造函數時,又會調用一次SuperType構造函數,這一次又在新對象上創建了實例屬性name和colors。于是,這兩個屬性就拼比了原型中的兩個同名屬性。
所謂寄生組合式繼承,即通過借用構造函數還繼承屬性,通過原型鏈的混成形式來繼承方法。其基本思路是:不必為了指定子類型的原型而調用超類型的構造函數,我們需要的無非是超類型原型的一個副本而已,本質上,就是使用寄生式來繼承超類型的原型,然后再將結結果指定給子類型的原型。寄生組合式繼承的基本模式如下
functiong inheritPrototype(SubType,SuperType){
var prototype = object(SuperType.prototype);
prototype.constructor = SubType;
SubType.prototype=prototype;
}
這個示例中 inheritPrototype()函數的語句,去替換前面例子中為子類型原型賦值的語句了。函數實現了寄生組合式繼承的最簡單形式。這個函數接收兩個參數:子類型構造函數和超類型構造函數。在函數內部,第一步是創建超類型原型的一個副本;第二部是為創建的副本添加constructor屬性,從而彌補因重寫爾失去的默認constructor尚需經;最后一步,將新創建的對象(即副本)賦值給子類型的原型。這樣,我們就可以調用inheritPrototype()函數的語句,去替換前面例子中為子類型原型賦值的語句了。
function SuperType(name){
this.name = name;
this.color = ['red','blue','green'];
}
SuperType.prototype.sayName = function(){
console.log(this.name);
}
function SubType(name,age){
SuperType.call(this,name); //第二次調用SuperType()
this.age =age;
}
inheritPrototype(SubType,SuperType);
Subtype.prototype.sayAge = function(){
console.log(this.age);
}
這個例子的高效率體現在它只調用了一次SuperType構造函數,并且因此避免了在SubType.prototype上面創建不必要的、多余的屬性。與此同時,原型鏈還能保持不變,因此,還能正常使用instanceof和isPrororypeOf().