javascript原型與繼承淺談二

原型的含義是指:如果構造器有個原型對象A,則由該構造器創建的實例(Object Instance)都必然複製於A。““在JavaScript中,對象實例(Object Instance)並沒有原型,而構造器(Constructor)有原型,屬性’<構造器>.prototype’指向原型。對象只有“構造自某個原型”的問題,並不存在“持有(或擁有)某個原型”的問題。””如何理解這一句話?

代碼1:

01 function myFunc() {
02     var name ="stephenchan";
03     var age = 23;
04     function code() {
05         alert("Hello World!");
06     };
07 }
08 var obj =new myFunc();
09 //輸出undefined,對象實例沒有原型
10 alert(obj.prototype);
11 //輸出myFunc的函數代碼,obj由myFunc構造出來的
12 alert(obj.constructor);
13 //輸出true
14 alert(obj.constructor == myFunc);
15  
16 //輸出[object Object],說明myFunc的原型是一個對象
17 alert(myFunc.prototype);
18 //輸出function Function() { [native code] },[native code]的意思是JavaScript引擎的內置函數
19 alert(myFunc.constructor);
20 //輸出true,函數原型的構造器默認是該函數
21 alert(myFunc.prototype.constructor == myFunc);

構造器與函數的概念是一致的,即代碼1中,myFunc就是一個構造器,因爲通過new myFunc()就可以構造出一個對象實例了。因此,”alert(obj.prototype)”輸出undefined說明了對象實例是沒有原型的,”alert(myFunc.prototype)”輸出[object Object]說明了構造器有原型,而“obj.constructor==myFunc”返回true說明obj的構造器是myFunc。

原型其實也是一個對象實例。再強調一下原型的含義是:如果構造器有個原型對象A,則由該構造器創建的實例(Object Instance)都必然複製於A,而且採用的讀遍歷機制複製的。讀遍歷複製的意思是:僅當寫某個實例的成員時,將成員的信息複製到實例映像中。即當構造一個新的對象時,新對象裏面的屬性指向的是原型中的屬性,讀取對象實例的屬性時,獲取的是原型對象的屬性值。而當對象實例對一個屬性進行寫操作時,纔會將屬性寫到新對象實例的屬性列表中。

prototype_build

圖1 JavaScript使用讀遍歷機制實現的原型繼承

代碼2:

01 Object.prototype.value ="abc";
02 var obj1 =new Object();
03 var obj2 =new Object();
04 obj2.value = 10;
05 //輸出abc,讀取的是原型Object中的value
06 alert(obj1.value);
07 //輸出10,讀取的是obj2成員列表中的value
08 alert(obj2.value);
09 //刪除obj2中的value,即在obj2的成員列表中將value刪除掉
10 delete obj2.value;
11 //輸出abc,讀取的是原型Object中的value
12 alert(obj2.value);

圖1是對代碼2的描述,說明讀遍歷機制是如何在成員列表以至原型中管理對象成員的。只有對屬性進行第一次寫操作的時候,纔會在對象的成員列表中添加該屬性的記錄。當obj1和obj2通過new來構造出來的時候,仍然是一個指向原型的引用,在操作過程中也沒有與原型相同大小的對象實例創建出來。這樣的讀遍歷就避免在創建新對象實例時可能的大量內存分配。當obj2.value屬性被賦值爲10的時候,obj2則在其成員表中添加了一個value成員,並賦值爲10,這個成員表就是記錄了obj2中發生了修改的成員名、值與類型。這張表是否與原型一致並不重要,只需要遵循兩條規則:(1)保證在讀取時被首先訪問到。(2)如果在對象中沒有指定的屬性,則嘗試遍歷對象的整個原型鏈,直到原型爲空或找到該屬性。代碼2中的delete操作是將obj2成員表中的value刪除了,因此在讀取obj2的value屬性的時候就遍歷到Object中讀取。

函數的原型總是一個標準的、系統內置的Object()構造器的實例,不過該實例創建後constructor屬性總先被賦值爲當前的函數。

代碼3:

01 function MyObject() {
02 }
03  
04 //顯示true,表明原型的構造器總是指向函數自身的
05 alert(MyObject.prototype.constructor == MyObject);
06  
07 //刪除該成員
08 delete MyObject.prototype.constructor;
09  
10 //刪除操作使該成員指向了父代類原型中的值
11 //均顯示爲true
12 alert(MyObject.prototype.constructor == Object);
13 alert(MyObject.prototype.constructor ==new Object().constructor);

從代碼3中可以看出,MyObject.prototype其實與一個普通對象”new Object()”並沒有本質的區別,只是在創建時將constructor賦值爲當前函數MyObject。然後,當一個函數的prototype有意義之後,它就搖身一變成了一個“構造器”,這時,如果用戶試圖用new運算符創建它的實例時,那麼引擎就會再構造一個新的對象,並使這個新對象的原型鏈接向這個prototype屬性就可以了。因此,函數與構造器並沒有明顯的界限

一個構造器產生的實例,其constructor屬性默認總是指向該構造器,而究其根源,則在於構造器(函數)的原型的constructor屬性指向了構造器本身。
代碼4:

1 function MyObject() {
2 }
3 var obj =new MyObject();
4 //輸出爲true,默認指向構造器
5 alert(obj.constructor == MyObject);
6 //輸出爲true,原型的構造器指向該構造器
7 alert(MyObject.prototype.constructor == MyObject);

由此可見,JavaScript事實上已經爲構造器維護了原型屬性,因此我們可以通過實例的constructor屬性來找到構造器,並進而找到它的原型“obj.constructor.prototype”。但是,如果我們把構造器的原型修改了的話,會出現什麼情況呢?如代碼5,我們把MyObjectEx的原型修改了。
代碼5:

1 function MyObject() {
2 }
3 function MyObjectEx() {
4 }
5 MyObjectEx.prototype =new MyObject();
6 var obj1 =new MyObject();
7 var obj2 =new MyObjectEx();
8 alert(obj1.constructor == obj2.constructor);   //true
9 alert(MyObjectEx.prototype.constructor == MyObject.prototype.constructor);   //true

在代碼5中,obj1和obj2是由不同的兩個構造器產生的實例,分別是MyObject和MyObjectEx。然而,我們看到,代碼5中的兩個alert都會輸出true,即是說,由兩個不相同的構造器產生的實例(代碼5中的MyObject和MyObjectEx),它們的constructor屬性卻指向了相同的構造器,是不是很詭異?這個正確是體現了原型繼承中出出現的“原型複製”了。要注意,MyObjectEx的原型是由MyObject構造出來的對象實例,即obj1和obj2都是從MyObject原型中複製出來的對象,因此它們的constructor指向的都是MyObject。那麼怎麼解決這個問題?
代碼6:

01 function MyObject() {
02     this.constructor = arguments.callee;//arguments.callee爲MyObject,正確維護constructor,以便回溯外部原型鏈
03 }
04 MyObject.prototype =new Object();//人爲構建外部原型鏈
05  
06 function MyObjectEx() {
07     this.constructor = arguments.callee;//正確維護constructor,以便回溯外部原型鏈
08 }
09 MyObjectEx.prototype =new MyObject();//人爲構建外部原型鏈
10  
11 obj1 =new MyObjectEx();
12 obj2 =new MyObjectEx();

代碼6與代碼5中的主要區別就是在於,在MyObjectEx的初始化中正確地維護了constructor屬性,使當前的constructor屬性指向了調用的構造器。代碼6所描述的繼承關係如圖2:

proto

圖2 構造器原型鏈與內部原型鏈

其中有[proto]屬性中一個對象的私有屬性,用於正確維護對象的內部原型鏈,在Firefox中可以通過[__proto__]來訪問,這個後面再討論。我們可以看到MyObjectEx的構造器是MyObject的對象實例,而MyObject的構造器是Object的對象實例。
接代碼6:

1 obj =new MyObject();
2 alert(obj.constructor === MyObject);   //true
3 alert(obj1.constructor === MyObjectEx);   //true
4 alert(obj.constructor === obj1.constructor);   //false

可以看到,obj和obj1從不同的構造器產生的實例,其constructor屬性已經能夠正確地指向相應的構造器,這個是由於在對象實例初始化的時候的賦值語句”this.constructor = arguments.callee;”。你可能會疑問爲什麼不採用下面這種方式來實現:

1 MyObjectEx.prototype =new MyObject();
2 MyObjectEx.prototype.constructor = MyObjectEx;

這樣雖然能使obj1和obj2的constructor屬性正確地指向了MyObjectEx,但是,這樣同時也使得MyObjectEx的原型對象(MyObject構造的實例)的constructor屬性沒法往父代原型追溯。因爲當MyObjectEx的原型對象想通過constructor屬性來獲取到MyObject構造器時,會發現獲取到的是MyObjectEx的構造器,而不是期待的MyObject的構造器。
我們可以通過下面的語句來驗證代碼6是不是的確是如圖2的關係鏈:

1 alert(obj1.constructor === MyObjectEx);//true
2 alert(MyObjectEx.prototypeinstanceof MyObject);//true
3 alert(MyObjectEx.prototype.constructor === MyObject);//true
4 alert(MyObject.prototypeinstanceof Object);//true
5 alert(MyObject.prototype.constructor === Object);//true
6 alert(obj1.constructor.prototype.constructor.prototype.constructor === Object);//true,完成了所有的回溯

好了,剛纔上面提到了有一個不可訪問的屬性[proto],這個屬性是JavaScript引擎內部維護的原型鏈屬性,這個屬性在Firefox裏面可以通過[__proto__]來訪問的,一般情況下,[proto]屬性指向的和prototype屬性一樣,指向的都是原型對象,兩個有什麼不同後面會有講述。

1 //輸出都是true,在Firefox中
2 alert(obj.__proto__instanceof Object);
3 alert(obj1.__proto__instanceof MyObject);
4 alert(obj2.__proto__instanceof MyObject);

這個[proto]屬性是JavaScript內部維護的,外部是不可訪問的,由這個屬性所維護的原型鏈爲內部原型鏈,與由prototype和constructor維護的外部原型鏈。那麼這兩條原型鏈有什麼區別呢?簡單來說就是,通過prototype和constructor來維護的外部原型鏈是開發人員自己代碼中回溯時用到的,而通過[proto]維護的內部原型鏈是JavaScript原型繼承機制實現所需要的。具體來說,外部原型鏈就是做這種事:”alert(obj1.constructor.prototype.constructor.prototype.constructor === Object);”,也就是說當我們開發人員想要自己去回溯整個原型繼承的結構鏈時,也只會在我們開發人員寫代碼時纔出現通過prototype和constructor來訪問外部原型鏈。而內部原型鏈,這個比較有意思,在[圖1 JavaScript使用讀遍歷機制實現的原型繼承],我們看到,當我們訪問一個對象實例的屬性時,它如果發現在其成員列表中沒有該屬性,即會去訪問原型的成員列表,把原型的默認值讀取出來,也就是說,這個在原型鏈中回溯來查詢成員屬性的過程,只會在內部原型鏈中進行,這個過程是由JavaScript引擎自己去維護的,開發人員沒法干涉。來看看代碼,我覺得這個還是相當有意思的:
接代碼6:

01 alert(obj.__proto__instanceof Object);   //true
02 alert(obj1.__proto__instanceof MyObject);   //true
03 alert(obj2.__proto__instanceof MyObject);   //true
04 //按照上面所說的,在MyObjectEx的原型上添加了value的屬性,那麼在訪問obj1和obj2的value屬性時便會往原型中查找
05 MyObjectEx.prototype.value ="Hello World!";
06 //這裏正確地輸出"Hello World!"
07 alert(obj1.value);
08 //在此時,obj1和obj2都構造之後,我把原來的MyObjectEx的原型換了,變成MyObjectEx2
09 function MyObjectEx2() {}
10 MyObjectEx.prototype =new MyObjectEx2();
11 //這句究竟會輸出什麼呢?[Referece Error]還是?
12 alert(obj1.value);

最後的1個alert輸出的”Hello World!”,有意思吧。即使我在上面把MyObjectEx的原型對象改變成新的MyObjectEx2,但是在obj1和obj2中的[proto]屬性依然指向的是原來的MyObject構造的對象實例,也就是說內部訪問屬性時是通過[proto]來回溯原型鏈的,而不是通過prototype的(而且對象實例也沒有prototype屬性),這個就是內部原型鏈體現的威力。

參考:

發佈了16 篇原創文章 · 獲贊 3 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章