原型的含義是指:如果構造器有個原型對象A,則由該構造器創建的實例(Object Instance)都必然複製於A。““在JavaScript中,對象實例(Object Instance)並沒有原型,而構造器(Constructor)有原型,屬性’<構造器>.prototype’指向原型。對象只有“構造自某個原型”的問題,並不存在“持有(或擁有)某個原型”的問題。””如何理解這一句話?
代碼1:
02 |
var name
= "stephenchan" ; |
05 |
alert( "Hello
World!" ); |
08 |
var obj
= new myFunc(); |
12 |
alert(obj.constructor); |
14 |
alert(obj.constructor
== myFunc); |
17 |
alert(myFunc.prototype); |
19 |
alert(myFunc.constructor); |
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,而且採用的讀遍歷機制複製的。讀遍歷複製的意思是:僅當寫某個實例的成員時,將成員的信息複製到實例映像中。即當構造一個新的對象時,新對象裏面的屬性指向的是原型中的屬性,讀取對象實例的屬性時,獲取的是原型對象的屬性值。而當對象實例對一個屬性進行寫操作時,纔會將屬性寫到新對象實例的屬性列表中。
圖1 JavaScript使用讀遍歷機制實現的原型繼承
代碼2:
01 |
Object.prototype.value
= "abc" ; |
02 |
var obj1
= new Object(); |
03 |
var obj2
= new Object(); |
圖1是對代碼2的描述,說明讀遍歷機制是如何在成員列表以至原型中管理對象成員的。只有對屬性進行第一次寫操作的時候,纔會在對象的成員列表中添加該屬性的記錄。當obj1和obj2通過new來構造出來的時候,仍然是一個指向原型的引用,在操作過程中也沒有與原型相同大小的對象實例創建出來。這樣的讀遍歷就避免在創建新對象實例時可能的大量內存分配。當obj2.value屬性被賦值爲10的時候,obj2則在其成員表中添加了一個value成員,並賦值爲10,這個成員表就是記錄了obj2中發生了修改的成員名、值與類型。這張表是否與原型一致並不重要,只需要遵循兩條規則:(1)保證在讀取時被首先訪問到。(2)如果在對象中沒有指定的屬性,則嘗試遍歷對象的整個原型鏈,直到原型爲空或找到該屬性。代碼2中的delete操作是將obj2成員表中的value刪除了,因此在讀取obj2的value屬性的時候就遍歷到Object中讀取。
函數的原型總是一個標準的、系統內置的Object()構造器的實例,不過該實例創建後constructor屬性總先被賦值爲當前的函數。
代碼3:
05 |
alert(MyObject.prototype.constructor
== MyObject); |
08 |
delete MyObject.prototype.constructor; |
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:
3 |
var obj
= new MyObject(); |
5 |
alert(obj.constructor
== MyObject); |
7 |
alert(MyObject.prototype.constructor
== MyObject); |
由此可見,JavaScript事實上已經爲構造器維護了原型屬性,因此我們可以通過實例的constructor屬性來找到構造器,並進而找到它的原型“obj.constructor.prototype”。但是,如果我們把構造器的原型修改了的話,會出現什麼情況呢?如代碼5,我們把MyObjectEx的原型修改了。
代碼5:
3 |
function MyObjectEx()
{ |
5 |
MyObjectEx.prototype
= new MyObject(); |
6 |
var obj1
= new MyObject(); |
7 |
var obj2
= new MyObjectEx(); |
8 |
alert(obj1.constructor
== obj2.constructor); |
9 |
alert(MyObjectEx.prototype.constructor
== MyObject.prototype.constructor); |
在代碼5中,obj1和obj2是由不同的兩個構造器產生的實例,分別是MyObject和MyObjectEx。然而,我們看到,代碼5中的兩個alert都會輸出true,即是說,由兩個不相同的構造器產生的實例(代碼5中的MyObject和MyObjectEx),它們的constructor屬性卻指向了相同的構造器,是不是很詭異?這個正確是體現了原型繼承中出出現的“原型複製”了。要注意,MyObjectEx的原型是由MyObject構造出來的對象實例,即obj1和obj2都是從MyObject原型中複製出來的對象,因此它們的constructor指向的都是MyObject。那麼怎麼解決這個問題?
代碼6:
02 |
this .constructor
= arguments.callee; |
04 |
MyObject.prototype
= new Object(); |
06 |
function MyObjectEx()
{ |
07 |
this .constructor
= arguments.callee; |
09 |
MyObjectEx.prototype
= new MyObject(); |
11 |
obj1
= new MyObjectEx(); |
12 |
obj2
= new MyObjectEx(); |
代碼6與代碼5中的主要區別就是在於,在MyObjectEx的初始化中正確地維護了constructor屬性,使當前的constructor屬性指向了調用的構造器。代碼6所描述的繼承關係如圖2:
圖2 構造器原型鏈與內部原型鏈
其中有[proto]屬性中一個對象的私有屬性,用於正確維護對象的內部原型鏈,在Firefox中可以通過[__proto__]來訪問,這個後面再討論。我們可以看到MyObjectEx的構造器是MyObject的對象實例,而MyObject的構造器是Object的對象實例。
接代碼6:
2 |
alert(obj.constructor
=== MyObject); |
3 |
alert(obj1.constructor
=== MyObjectEx); |
4 |
alert(obj.constructor
=== obj1.constructor); |
可以看到,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); |
2 |
alert(MyObjectEx.prototype instanceof MyObject); |
3 |
alert(MyObjectEx.prototype.constructor
=== MyObject); |
4 |
alert(MyObject.prototype instanceof Object); |
5 |
alert(MyObject.prototype.constructor
=== Object); |
6 |
alert(obj1.constructor.prototype.constructor.prototype.constructor
=== Object); |
好了,剛纔上面提到了有一個不可訪問的屬性[proto],這個屬性是JavaScript引擎內部維護的原型鏈屬性,這個屬性在Firefox裏面可以通過[__proto__]來訪問的,一般情況下,[proto]屬性指向的和prototype屬性一樣,指向的都是原型對象,兩個有什麼不同後面會有講述。
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); |
02 |
alert(obj1.__proto__ instanceof MyObject); |
03 |
alert(obj2.__proto__ instanceof MyObject); |
05 |
MyObjectEx.prototype.value
= "Hello
World!" ; |
09 |
function MyObjectEx2()
{} |
10 |
MyObjectEx.prototype
= new MyObjectEx2(); |
最後的1個alert輸出的”Hello World!”,有意思吧。即使我在上面把MyObjectEx的原型對象改變成新的MyObjectEx2,但是在obj1和obj2中的[proto]屬性依然指向的是原來的MyObject構造的對象實例,也就是說內部訪問屬性時是通過[proto]來回溯原型鏈的,而不是通過prototype的(而且對象實例也沒有prototype屬性),這個就是內部原型鏈體現的威力。
參考: