??? 在JavaScript當中,對象A如果要繼承對象B的屬性和方法,那么只要將對象B放到對象A的原型鏈上即可。而某個對象的原型鏈,就是由該對象開始,通過__proto__屬性連接起來的一串對象。__proto__屬性是JavaScript對象中的內部屬性,任何JavaScript對象,包括我們自己構建的對象,JavaScript的built-in對象,任何函數(在JavaScript當中,函數也是對象)都具有這個屬性。如下圖就是一個原型鏈的例子:
??? 上圖中,A,B,C分別代表3個對象,藍色箭頭串接起來的所有對象就構成了對象C的原型鏈,其中C的_proto__屬性指向B,B的__proto__屬性指向A,A的__proto__屬性可能指向更高層的對象,也可能指向null(表示A不繼承任何對象的屬性和方法)。如果我們引用了C的某個屬性或者方法,那么JavaScript就會順著C的原型鏈進行查找,即首先查找對象C本身,看所引用的屬性名或者方法名是否存在,如果存在就停止查找直接返回,如果不存在,就通過C的__proto__屬性找到原型鏈中的B對象,繼續在B對象中查找,如果B對象中找到所引用的屬性名或者方法名,那么就停止查找直接返回,如果B對象中也不存在,就通過對象B的__proto__屬性找到原型鏈中的A對象,繼續重復上述查找過程,直到找到所引用的屬性或者方法為止(同時也可能查找完對象C的整個原型鏈也沒有找到所引用的屬性或者方法,那么該屬性或者方法就是undefined的)。
因此,只要能夠成功的為某一個對象構造出我們需要的原型鏈,那么就能讓該對象繼承我們想要它繼承的方法或者屬性。而想要成功構造對象的原型鏈,就還必須理解prototype屬性,JavaScript當中已經存在的原型鏈,以及當我們創建對象時,原型鏈被構造的過程。
?
prototype屬性
prototype屬性存在于JavaScript的任何函數當中,這個屬性指向的對象就是所謂的原型對象,在構造原型鏈時需要原型對象。
?
JavaScript當中已經存在的原型鏈
在JavaScript當中存在Object,Function,Array,String,Boolean,Number,Date,Error,RegExp這9個built-in函數一個built-in的Math對象,通過這上述9個built-in函數我們可以創建相應的對象,同時,這9個built-in函數的prototype屬性所指向的原型對象也是built-in的。下面的圖示解釋了這幾個函數以及各自prototype屬性所指向的原型對象之間的關系。
(如果此圖看不清,可點擊此處下載)
上面的圖示中,黃色方框代表built-in函數對象,深綠色方框代表built-in函數prototype屬性指向的原型對象,名字都叫xx prototype object,淺綠色方框(即Math對象)代表普通對象,藍色箭頭連接非built-in函數對象(無論是普通對象如Math,還是原型對象)的__proto__屬性,而土黃色箭頭連接函數對象的__proto__屬性。
通過上圖可以發現,所有built-in函數對象的原型鏈最終都指向Function prototype object,所有非函數對象的原型鏈最終都指向Object prototype object,并且Function prototype object的__proto__屬性也指向Object prototype object,Object prototype object的__proto__屬性指向為null。因此,Object prototype object是所有原型鏈的頂端。因此,通過原型鏈查找規則可知,所有built-in函數對象同時繼承了Object prototype object和Function prototype object上的屬性和方法,而所有非built-in函數對象只繼承了Object prototype object上的方法。Function prototype object包含了所有函數共享的屬性和方法,而Object prototype object包含了所有對象都共享額屬性和方法。
對于上圖中原型對象包含的constructor屬性,下文當中有解釋。
?
創建對象時,原型鏈的構造過程
在JavaScript當中創建對象有2中方式,一種是通過定義函數使用new方法來構造,另一種是使用對象字面量的方式,即:
var obj = {name: "Jim Green" };
使用這兩種方式創建對象時,對象的原型鏈構造過程有所不同。
1 使用函數的方式構造對象
使用函數的方式構造對象分為兩步:首先需要定義一個函數作為構造函數,然后使用new方法構造對象。接下來就來看一下這兩個步驟會發生什么。
假設我們定義了一個函數名為F才,此時JavaScript會為我們做兩件事,第一:根據我們定義的函數創建一個函數對象,第二,設置這個函數的__proto__屬性和prototype屬性。其中__proto__屬性指向built-in的Function prototype object,而prototype屬性指向一個為函數對象F新創建的原型對象,這個新創建的原型對象通過調用new Object()構造出來,并且為這個新創建的對象添加constructor屬性,該屬性指向函數對象F。最后的結果如下圖所示:
?
上圖中為了方便,沒有畫出Function prototype object和Object prototype object的constructor屬性。而F prototype object的__proto__屬性為何指向Object prototype object,下文介紹new操作符時有解釋。
?
當我們使用new方法調用F函數的時候,JavaScript也會為我們做兩件事,第一,分配內存作為新創建的對象,第二,將新創建的對象的__proto__屬性指向函數F的原型對象,結果如下圖:
上圖中,obj就是調用new方法通過函數F創建出來的對象,我們可以看到對象obj的原型鏈包含了函數F的原型對象,以及Object prototype object,這樣,對象obj通過原型鏈查找規則,就能繼承函數F的原型對象,以及Object prototype object上面定義的屬性和方法了。并且如果我們想知道一個對象是由哪個方法構建的,只需要訪問這個對象的constructor屬性即可,上例中,只要我們訪問obj.constructor,那么就知道obj是由函數F創建的。同時,由于F prototype object上文中介紹是由new Object函數創建的,根據此處介紹,F prototype object的__proto__屬性應該指向Object函數的原型對象,即Object prototype object。
2 使用對象字面量定義對象
當使用對象字面量創建對象時,JavaScript會為我們做兩件事:
1 分配內存作為新創建的對象
2 將新創建對象的__proto__屬性指向Object prototype object
結果如下圖所示:
?
上圖為了簡化,同樣沒有畫出Object prototype object的constructor屬性
?
繼承
理解了上面所講的原理之后,假設目前有一個對象A(這個對象可以是任意的,包括JavaScript built-in的對象,任何函數對象,任何原型對象,以及我們自己new出來的對象),現在想創建一個對象obj,讓obj繼承A的屬性和方法。通過上面的介紹,我們知道創建對象有兩種方式,但是使用對象字面量創建的對象其原型鏈總是只包含兩個對象,一個是其自己,一個是Object prototype object,根本不可能包含對象A,無法達到讓對象obj繼承對象A屬性和方法的效果。因此,只能使用函數的方式創建對象,讓對象A包含在新創建對象obj的原型鏈中即可。根據上面的講解,如果是用函數的方式創建對象,那么在調用new方法時,新創建對象的__proto__屬性會指向函數的原型對象。因此,只要在調用函數之前,將函數的原型對象換成A,然后再調用new方法,就可以將對象A包含在新創建的對象obj的原型鏈中,這樣通過原型鏈查找規則,obj就繼承了A的屬性和方法。假設用來創建對象obj的函數為B,則相關代碼為:
B.prototype = A; B.prototype.constructor = B; var obj = new B(傳入的參數)
上面代碼中的B.prototype.constructor = B,是因為對象A中可能沒有constructor屬性,或者constructor屬性不指向B,而為了方便通過訪問任何由B函數創建的對象的constructor屬性,就可以正確的知道該對象是使用函數B構造出來的。相關圖示如下圖:
上圖中虛線框所包圍的B prototype object就是定義函數B時,JavaScript為函數B生成的原型對象,但是該對象被我們用代碼替換成了對象A。由于這個被替換的B prototype object沒有其他地方再用到,因此會被回收掉。
?
參考資料:
?EcmaScript Language Specification 5
?