JavaScript之繼承(原型鏈)

  咱們知道繼承是oo語言中不可缺乏的一部分,對於JavaScript也是如此。通常的繼承有兩種方式:其一,接口繼承,只繼承方法的簽名;其二,實現繼承,繼承實際的方法。JavaScript不支持簽名,所以只有實現繼承。其中實現繼承主要是依賴於原型鏈的。下面我將以原型鏈爲重點說說繼承的幾種主要的方式:html

  • 原型鏈繼承
  • 借用構造函數繼承
  • 組合繼承(重點)

第一部分:原型鏈繼承

  A數組

  要說原型鏈繼承,不得不首先介紹一下原型鏈的概念。app

  想象一下,若是使原型對象等於另外一個對象的實例,則此時原型對象將包含一個指向另外一個原型的指針。相應地,另外一個原型也將包含指向另外一個構造函數的指針。假設另外一個原型又是另外一個類型的實例,那麼上述關係依然成立,如此層層遞進,就構成了實例與原型的鏈條(注意:這裏的實例和原型都是相對的),這即是原型鏈的基本概念。函數

  

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function  SuperType(){
     this .property= true ;
}
SuperType.prototype.getSuperValue= function (){
     return  this .property;
};
function  SubType(){
     this .subproperty= false ;
}
SubType.prototype= new  SuperType();
SubType.prototype.getSubvalue= function (){
     return  this .subproperty;
}
var  instance= new  SubType();
console.log(instance.getSuperValue()); //true

  在上述代碼中,咱們能夠看出subType的原型是SuperType的實例,所以,原來存在於SuperType的實例中的全部屬性和方法,如今也存在於SubType.prototype中了。且咱們沒有使用SubType默認提供的原型對象,而是給它換了一個新原型對象(即SuperType的實例)。所以,新原型對象不只具備做爲一個SuperType的實例所擁有的所有屬性和方法,並且其內部還有一個指針,指向了SuperType的原型。即:instance指向SubType的原型,SubType的原型指向了SuperType的原型。值得注意的是:property如今位於SubType.protoType中(由於SuperType構造函數中的this指向的是建立的對象實例)。性能

  當以讀取模式訪問一個實例屬性時,搜索過程會沿着原型鏈向上進行搜索。好比,調用instance.getSuperValue()會經歷三個搜索步驟:(1).搜索實例中是否存在該方法,結果:無。(2).沿着原型鏈向上,搜索SubType.prototype中是否存在該方法,結果:無。(3).繼續沿着原型鏈,搜索SuperType.prototype中是否存在該方法,結果:存在。因而中止搜索。也就是說:在找不到屬性或方法的狀況下,搜索過程老是要一環一環地前行到原型鏈末端纔會停下來。this

   注意:instance.constructor如今指向的是SuperType,這是由於SubType的原型指向了另外一個對象--SuperType的原型,而這個原型對象的constructor屬性指向的是SuperType。咱們能夠用如下代碼作出驗證:spa

1 console.log(instance.constructor);

  最終返回的是SuperType這個構造函數。prototype

  重要:別忘記默認的原型。咱們知道,全部的引用類型都繼承了Object,而這個繼承也是經過原型鏈實現的,即全部函數的默認原型都是Object的實例,所以默認原型都會包含一個內部指針,指向Object.prototype。這也是全部引用類型都會繼承toString()、valueOf()方法的根本緣由。咱們可使用下面代碼作出驗證:指針

1
2
3
console.log(Object.prototype.isPrototypeOf(instance)); //true
console.log(SuperType.prototype.isPrototypeOf(instance)); //true
console.log(SubType.prototype.isPrototypeOf(instance)); //true

  也就是說instace實例對象的原型對象分別是Object.prototype、SuperType.prototype、SubType.prototype。另外咱們還可使用instanceof操做符判斷,實例instance與構造函數之間的關係,以下所示:code

1
2
3
console.log(instance  instanceof  Object); //true
console.log(instance  instanceof  SuperType); //true
console.log(instance  instanceof  SubType); //true

  即instance是Object SuperType SubType的實例。下面咱們使用一張圖表表示他們之間的關係。

  這裏,咱們能夠認爲加粗的線條就是原型鏈(實例與原型的鏈條)。

  從這張圖表中,咱們能夠看到SubType Prototype是沒有constructer屬性的,更沒有指向SubType構造函數,這是由於建立SubType構造函數同時建立的原型對象和這個原型對象不是同一個,這個原型對象是SuperType的實例。注意到,後兩個原型對象都有一個[[prototype]]屬性,由於這時他們是被看成實例來處理的。

 

  B

  謹慎地定義方法

  當子類型有時候須要覆蓋(與原型中覆蓋屬性是一樣的道理,見《深刻理解JavaScript中建立對象模式的演變(原型)》)超類型的某個方法,或者須要添加超類型中不存在的某個方法。這時,應當注意:給原型添加方法的代碼必定要放在(用超類型的對象實例做爲子類型的原型來)替換原型的語句以後。看如下代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
     function  SuperType(){
     this .property= true ;
}
SuperType.prototype.getSuperValue= function (){
     return  this .property;
};
function  SubType(){
     this .subproperty= false ;
}
SubType.prototype= new  SuperType(); //這一句代碼即爲替換的原型的語句
SubType.prototype.getSubValue= function (){
     return  this .subproperty; //這時在子類型中新添加的方法
}
SubType.prototype.getSuperValue= function (){
     return  false ; //這時在子類型添加的超類型的同名方法,用於覆蓋超類型中的方法,所以,最後反悔了false
}
var  instance=  new  SubType();
console.log(instance.getSuperValue()); //false

  

  若是順序顛倒,那麼這兩個新添加的方法就是無效的了,最終instance.getSuperValue()獲得的結果仍然是從超類型中搜索到的,返回false這時由於若是顛倒,那麼後面添加的方法給了SubType最開始的原型,後面替換原型以後,就只能繼承超類型的,而剛剛添加的方法不會被實例所共享,此時實例的[[prototype]]指向的是替換以後的原型對象而不在指向最初的添加了方法的原型對象。

  還有一點須要注意的就是,在經過原型鏈實現繼承時,不能使用對象字面量建立原型方法(這樣就會再次建立一個原型對象,而不會剛剛的那個用超類型的實例替換的對象),由於這樣會切斷原型鏈,沒法實現繼承。

  C

  單獨使用原型鏈的問題

 問題1: 最主要的問題是當包含引用類型值的原型。首先,回顧如下原型模式建立對象的方法,對於包含引用類型值的原型屬性會被全部的實例共享,這樣改變其中一個實例,其餘都會被改變,這不是咱們想要的。這也正是以前關於原型的講解中爲何要將引用類型的值定義在構造函數中而不是定義在原型對象中。對於原型鏈,也是一樣的問題。

  看如下的代碼;

   

1
2
3
4
5
6
7
8
9
10
     function  SuperType(){
    this .colors=[ "red" , "blue" , "green" ];
}
function  SubType(){}
SubType.prototype= new  SuperType(); //這時,SuperType中的this對象指向的是SubType.prototype
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構造函數中的this必定是指向由他建立的新對象的,而SubType.prototype正是這個新對象,所以SubType的原型對象便有了colors屬性,因爲這個屬性值是數組(引用類型),於是儘管咱們的本意是向instance1中添加一個「black」,但最終不可避免的影響到了instance2。而colors放在構造函數中有問題,若是放在其餘的原型對象中,依然會有問題。所以,這是原型鏈繼承的一個問題。

  

  問題二:

  在建立子類型的實例時,不能向超類型的構造函數傳遞參數。實際上,應該說沒有辦法在不影響全部對象實例的狀況下,給超類型的構造函數傳遞參數。

  正由於單單使用原型鏈來實現繼承出現的以上兩個問題,咱們在實踐中不多會單獨使用原型鏈。

 

 

第二部分:借用構造函數繼承

  A

  爲解決以上問題,人們發明了借用構造函數(又稱僞造對象或經典繼承),這種方法的核心思想是:在子類型構造函數的內部調用超類型構造函數。因爲函數只不過是在特定環境中執行代碼的對象,所以經過使用apply()和call()方法也能夠在(未來)新建立的對象上執行構造函數。注意:這種繼承方式沒有用到原型鏈的知識,與基於原型鏈的繼承毫無關係。代碼以下:

1
2
3
4
5
6
7
8
9
10
11
function  SuperType(){
     this .colors=[ "red" , "blue" , "green" ];
}
function  SubType(){
     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"]

  首先,咱們能夠看到此種繼承方式既完成了繼承任務,又達到了咱們但願達到的效果:對一個實例的值爲引用類型的屬性的修改不影響另外一個實例的引用類型的屬性值。

  值得注意的是:這種繼承方式與原型鏈的繼承方式是徹底不一樣的。看如下代碼:

1
2
console.log(instance1  instanceof  SubType); //true
console.log(instance1  instanceof  SuperType); //false

  instance1和instance2都不是SuperType的實例。這裏的繼承只是表面上的繼承。咱們能夠分析一下這個繼承的過程:首先聲明瞭兩個構造函數,而後執行var instance1=new SubType();即經過new調用了構造函數SubType,既然調用了SubType構造函數,此時便進入了SubType執行環境,該環境中又調用了SuperType()函數(注意:這裏未使用new,故此時應當把SuperType函數看成通常函數來處理),又由於SubType()中this是指向instance1(SubType是構造函數啊!)的,因此,接下來就會在instance1對象上調用普通函數SuperType,由於這個普通函數在instance1上被調用,所以,SuperType中的this又指向了Instance1,這是,instance1對象便添加了屬性值爲應用類型的colors屬性,instance2同理。

  這解決了原型鏈繼承中的第一個問題。

  

  B

  相對於原型鏈而言,借用構造函數有一個很大的優點,便可以在子類型構造函數中向超類型構造函數傳遞參數。以下所示:

1
2
3
4
5
6
7
8
9
10
function  SuperType(name){
     this .name=name;
}
function  SubType(){
     SuperType.call( this , "zzw" );
     this .age=21;
}
var  instance1= new  SubType();
console.log(instance1.name); //zzw
console.log(instance1.age); //21

  其中SuperType.call(this,"zzw");又能夠寫作SuperType.apply(this,["zzw"]);(關於這一部分知識點能夠看《JavaScript函數之美~》第三部分)。

  言歸正傳,讓咱們先分析函數時如何執行的:首先聲明瞭兩個構造函數,而後經過new操做符調用了Subtype構造函數,隨即進入Subtype構造函數的執行環境,執行語句SuperType.call(this.zzw);,隨即進入了普通函數(一樣地,只要沒有使用new操做符,它就是通常函數)的執行環境並傳遞了參數,且使用了call方法,說明在instance1對象上調用普通函數SuperType,由於在對象上調用的,因此SuperType函數中的this指向instance1,並最終得到了name屬性。SuperType函數執行環境中的代碼執行完畢以後,執行環境又回到了SubType構造函數,這時,instance對象又得到了屬性值爲21的age屬性。

  ok!借用構造函數繼承又解決了原型鏈繼承的第二個問題。

  然而,借用構造函數就沒有缺點嗎?答案是有!由於僅僅使用借用構造函數,就沒法避免構造函數模式的問題--方法在構造函數中定義(而致使浪費)。並且,咱們說這種方式與原型鏈不一樣,所以在超類型的原型中定義的方法,對子類型而言也是不可見的,結果全部類型都只能使用構造函數模式。

  考慮到上述問題,借用構造函數的技術也是不多單獨使用的。

 

 

第三部分:組合繼承(僞經典繼承)

  與建立對象時,咱們將自定義構造函數模式和原型模式組合同樣,這種繼承方式即將原型鏈和借用構造函數的技術組合到一塊兒,從而發揮二者之長。主要思想是:使用原型鏈實現對原型屬性(即但願讓各個實例共享的屬性)和方法(對於借用構造函數,繼承方法顯然是不合適的)的繼承,而經過借用構造函數來實現對實例屬性(即不但願共享的屬性,以前方法是經過實例屬性覆蓋原型屬性)的繼承。這樣,既經過在原型上定義方法實現了函數複用(即只建立一次方法,被屢次使用,若是將函數定義在構造函數中,建立一個實例,就會同時建立一個相同的方法,沒法複用,影響性能),又可以保證每一個實例都有本身的屬性(由於借用構造函數能夠傳遞參數啊!把實例屬性經過借用構造函數實現,就不用去覆蓋了)。

  

下面來看這樣一個例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function  SuperType(name,age){
     this .name=name; //實例屬性使用借用構造函數模式               this.age=age;//實例屬性使用借用構造函數模式
     this .colors=[ "red" , "blue" , "green" ]; //這個數組雖然會同時被原型鏈和借用構造函數添加使用,但最後根據原型鏈的搜索機制,是按照借用構造函數模式實現的。
}
SuperType.prototype.sayName= function (){
     console.log( this .name); //實現一樣效果的方法使用原型鏈模式
};
function  SubType(name,age){
     SuperType.call( this ,name,age); //借用構造函數模式的有點就是能夠向子類型構造函數中的超類型構造函數傳遞參數,這裏this的用法很重要
     
};
SubType.prototype= new  SuperType(); //使用SuperType的實例來替換爲SubType的原型對象
SubType.prototype.constructor=SubType; // 這句代碼即將SubType的原型對象的constructor屬性指向SubType,但這一句去掉也不會影響結果。
SubType.prototype.sayAge= function (){
     console.log( this .age); //在原型對象中定義方法,可使得該方法實現複用,加強性能
};
var  instance1= new  SubType( "zzw" ,21);
instance1.colors.push( "black" );
console.log(instance1.colors); //["red", "blue", "green", "black"]
instance1.sayName(); //zzw
instance1.sayAge(); //21
var  instance2= new  SubType( "ht" ,18);
console.log(instance2.colors); //["red", "blue", "green"]
instance2.sayName(); //ht
instance2.sayAge(); //18

 

  關鍵點:在SuperType構造函數中代碼this.colors=["red","blue","green"];實際上也會向單獨的原型鏈繼承那樣,將colors數組添加到SubType的原型對象中去,可是借用構造函數在執行時會將colors數組直接添加給實例,因此,訪問colors數組時,根據原型鏈的搜索機制,在實例中的colors數組一旦被搜索到,就不會繼續沿着原型鏈向上搜索了(屏蔽做用)。所以最終instance1的colors的改變並不會影響到instance2的colors數組的改變(二者的colors數組都來自實例自己而不是原型對象)。

 

 

 

  只會幻想而不行動的人,永遠也體會不到收穫果實時的喜悅。 Just do it!

相關文章
相關標籤/搜索