JavaScript的原型鏈

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

代碼1:ui

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,並且採用的讀遍歷機制複製的。讀遍歷複製的意思是:僅當寫某個實例的成員時,將成員的信息複製到實例映像中。即當構造 一個新的對象時,新對象裏面的屬性指向的是原型中的屬性,讀取對象實例的屬性時,獲取的是原型對象的屬性值。而當對象實例對一個屬性進行寫操做時,纔會將 屬性寫到新對象實例的屬性列表中。this

prototype_build

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

代碼2:prototype

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中讀取。code

函數的原型老是一個標準的、系統內置的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屬性就能夠了。所以,函數與構造器並無明顯的界限ip

一個構造器產生的實例,其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.prototype instanceof MyObject); //true
3 alert(MyObjectEx.prototype.constructor === MyObject); //true
4 alert(MyObject.prototype instanceof 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屬性),這個就是內部原型鏈體現的威力。

相關文章
相關標籤/搜索