歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux編程 >> Linux編程 >> JavaScript之繼承(原型鏈)

JavaScript之繼承(原型鏈)

日期:2017/3/1 9:10:00   编辑:Linux編程

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

原型鏈繼承
借用構造函數繼承
組合繼承(重點)
第一部分:原型鏈繼承

  A

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

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

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中是否存在該方法,結果:存在。於是停止搜索。也就是說:在找不到屬性或方法的情況下,搜索過程總是要一環一環地前行到原型鏈末端才會停下來。

   注意:instance.constructor現在指向的是SuperType,這是因為SubType的原型指向了另一個對象--SuperType的原型,而這個原型對象的constructor屬性指向的是SuperType。我們可以用以下代碼做出驗證:

console.log(instance.constructor);

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

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

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與構造函數之間的關系,如下所示:

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中創建對象模式的演變(原型)》)超類型的某個方法,或者需要添加超類型中不存在的某個方法。這時,應當注意:給原型添加方法的代碼一定要放在(用超類型的對象實例作為子類型的原型來)替換原型的語句之後。看以下代碼:

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: 最主要的問題是當包含引用類型值的原型。首先,回顧以下原型模式創建對象的方法,對於包含引用類型值的原型屬性會被所有的實例共享,這樣改變其中一個實例,其他都會被改變,這不是我們想要的。這也正是之前關於原型的講解中為什麼要將引用類型的值定義在構造函數中而不是定義在原型對象中。對於原型鏈,也是同樣的問題。

  看以下的代碼;

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()方法也可以在(將來)新創建的對象上執行構造函數。注意:這種繼承方式沒有用到原型鏈的知識,與基於原型鏈的繼承毫無關系。代碼如下:

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"]

  首先,我們可以看到此種繼承方式既完成了繼承任務,又達到了我們希望達到的效果:對一個實例的值為引用類型的屬性的修改不影響另一個實例的引用類型的屬性值。

  值得注意的是:這種繼承方式與原型鏈的繼承方式是完全不同的。看以下代碼:

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

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

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函數之美~ http://www.linuxidc.com/Linux/2016-11/136882.htm》第三部分)。

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

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

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

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

第三部分:組合繼承(偽經典繼承)
  與創建對象時,我們將自定義構造函數模式和原型模式組合一樣,這種繼承方式即將原型鏈和借用構造函數的技術組合到一起,從而發揮兩者之長。主要思想是:使用原型鏈實現對原型屬性(即希望讓各個實例共享的屬性)和方法(對於借用構造函數,繼承方法顯然是不合適的)的繼承,而通過借用構造函數來實現對實例屬性(即不希望共享的屬性,之前方法是通過實例屬性覆蓋原型屬性)的繼承。這樣,既通過在原型上定義方法實現了函數復用(即只創建一次方法,被多次使用,如果將函數定義在構造函數中,創建一個實例,就會同時創建一個相同的方法,無法復用,影響性能),又能夠保證每個實例都有自己的屬性(因為借用構造函數可以傳遞參數啊!把實例屬性通過借用構造函數實現,就不用去覆蓋了)。

下面來看這樣一個例子:

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數組都來自實例本身而不是原型對象)。

Copyright © Linux教程網 All Rights Reserved