標籤:開始 判斷 自己 jpg 替換 答案 基本 組合 介面
我們知道繼承是oo語言中不可缺少的一部分,對於JavaScript也是如此。一般的繼承有兩種方式:其一,介面繼承,只繼承方法的簽名;其二,實現繼承,繼承實際的方法。JavaScript不支援簽名,因此只有實現繼承。其中實現繼承主要是依賴於原型鏈的。下面我將以原型鏈為重點說說繼承的幾種主要的方式:
第一部分:原型鏈繼承
A
要說原型鏈繼承,不得不首先介紹一下原型鏈的概念。
想象一下,如果使原型對象等於另一個對象的執行個體,則此時原型對象將包含一個指向另一個原型的指標。相應地,另一個原型也將包含指向另一個建構函式的指標。假設另一個原型又是另一個類型的執行個體,那麼上述關係依然成立,如此層層遞進,就構成了執行個體與原型的鏈條(注意:這裡的執行個體和原型都是相對的),這便是原型鏈的基本概念。
| 123456789101112131415 |
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。我們可以用以下代碼做出驗證:
| 1 |
console.log(instance.constructor); |
最終返回的是SuperType這個建構函式。
重要:別忘記預設的原型。我們知道,所有的參考型別都繼承了Object,而這個繼承也是通過原型鏈實現的,即所有函數的預設原型都是Object的執行個體,因此預設原型都會包含一個內部指標,指向Object.prototype。這也是所有參考型別都會繼承toString()、valueOf()方法的根本原因。我們可以使用下面代碼做出驗證:
| 123 |
console.log(Object.prototype.isPrototypeOf(instance));//trueconsole.log(SuperType.prototype.isPrototypeOf(instance));//trueconsole.log(SubType.prototype.isPrototypeOf(instance));//true |
也就是說instace執行個體對象的原型對象分別是Object.prototype、SuperType.prototype、SubType.prototype。另外我們還可以使用instanceof操作符判斷,執行個體instance與建構函式之間的關係,如下所示:
| 123 |
console.log(instance instanceof Object);//trueconsole.log(instance instanceof SuperType);//trueconsole.log(instance instanceof SubType);//true |
即instance是Object SuperType SubType的執行個體。下面我們使用一張圖表表示他們之間的關係。
這裡,我們可以認為加粗的線條就是原型鏈(執行個體與原型的鏈條)。
從這張圖表中,我們可以看到SubType Prototype是沒有constructer屬性的,更沒有指向SubType建構函式,這是因為建立SubType建構函式同時建立的原型對象和這個原型對象不是同一個,這個原型對象是SuperType的執行個體。注意到,後兩個原型對象都有一個[[prototype]]屬性,因為這時他們是被當作執行個體來處理的。
B
謹慎地定義方法
當子類型有時候需要覆蓋(與原型中覆蓋屬性是同樣的道理,見《深入理解JavaScript中建立對象模式的演變(原型)》)超類型的某個方法,或者需要添加超類型中不存在的某個方法。這時,應當注意:給原型添加方法的代碼一定要放在(用超類型的對象執行個體作為子類型的原型來)替換原型的語句之後。看以下代碼:
| 12345678910111213141516171819 |
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: 最主要的問題是當包含參考型別值的原型。首先,回顧以下原型模式建立對象的方法,對於包含參考型別值的原型屬性會被所有的執行個體共用,這樣改變其中一個執行個體,其他都會被改變,這不是我們想要的。這也正是之前關於原型的講解中為什麼要將參考型別的值定義在建構函式中而不是定義在原型對象中。對於原型鏈,也是同樣的問題。
看以下的代碼;
| 12345678910 |
function SuperType(){ this.colors=["red","blue","green"];}function SubType(){}SubType.prototype=new SuperType();//這時,SuperType中的this對象指向的是SubType.prototypevar 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()方法也可以在(將來)新建立的對象上執行建構函式。注意:這種繼承方式沒有用到原型鏈的知識,與基於原型鏈的繼承毫無關係。代碼如下:
| 1234567891011 |
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"] |
首先,我們可以看到此種繼承方式既完成了繼承任務,又達到了我們希望達到的效果:對一個執行個體的值為參考型別的屬性的修改不影響另一個執行個體的參考型別的屬性值。
值得注意的是:這種繼承方式與原型鏈的繼承方式是完全不同的。看以下代碼:
| 12 |
console.log(instance1 instanceof SubType);//trueconsole.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
相對於原型鏈而言,借用建構函式有一個很大的優勢,即可以在子類型建構函式中向超類型建構函式傳遞參數。如下所示:
| 12345678910 |
function SuperType(name){ this.name=name;}function SubType(){ SuperType.call(this,"zzw"); this.age=21;}var instance1=new SubType();console.log(instance1.name);//zzwconsole.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!借用建構函式繼承又解決了原型鏈繼承的第二個問題。
然而,借用建構函式就沒有缺點嗎?答案是有!因為僅僅使用借用建構函式,就無法避免建構函式模式的問題--方法在建構函式中定義(而導致浪費)。而且,我們說這種方式與原型鏈不同,因此在超類型的原型中定義的方法,對子類型而言也是不可見的,結果所有類型都只能使用建構函式模式。
考慮到上述問題,借用建構函式的技術也是很少單獨使用的。
第三部分:組合繼承(偽經典繼承)
與建立對象時,我們將自訂建構函式模式和原型模式組合一樣,這種繼承方式即將原型鏈和借用建構函式的技術組合到一起,從而發揮兩者之長。主要思想是:使用原型鏈實現對原型屬性(即希望讓各個執行個體共用的屬性)和方法(對於借用建構函式,繼承方法顯然是不合適的)的繼承,而通過借用建構函式來實現對執行個體屬性(即不希望共用的屬性,之前方法是通過執行個體屬性覆蓋原型屬性)的繼承。這樣,既通過在原型上定義方法實現了函數複用(即只建立一次方法,被多次使用,如果將函數定義在建構函式中,建立一個執行個體,就會同時建立一個相同的方法,無法複用,影響效能),又能夠保證每個執行個體都有自己的屬性(因為借用建構函式可以傳遞參數啊!把執行個體屬性通過借用建構函式實現,就不用去覆蓋了)。
下面來看這樣一個例子:
| 12345678910111213141516171819202122232425 |
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();//zzwinstance1.sayAge();//21var instance2=new SubType("ht",18);console.log(instance2.colors);//["red", "blue", "green"]instance2.sayName();//htinstance2.sayAge();//18 |
關鍵點:在SuperType建構函式中代碼this.colors=["red","blue","green"];實際上也會向單獨的原型鏈繼承那樣,將colors數組添加到SubType的原型對象中去,但是借用建構函式在執行時會將colors數組直接添加給執行個體,所以,訪問colors數組時,根據原型鏈的搜尋機制,在執行個體中的colors數組一旦被搜尋到,就不會繼續沿著原型鏈向上搜尋了(屏蔽作用)。因此最終instance1的colors的改變並不會影響到instance2的colors數組的改變(兩者的colors數組都來自執行個體本身而不是原型對象)。
只會幻想而不行動的人,永遠也體會不到收穫果即時的喜悅。 Just do it!
JavaScript之繼承(原型鏈)