一、函數創建過程
在了解原型鏈之前我們先來看看一個函數在創建過程中做了哪些事情,舉一個空函數的例子:
1 function A() {};
當我們在程式碼裡面宣告這麼一個空函數,js解析的本質是(膚淺理解有待深入):
1、創建一個物件(有constructor屬性及[[Prototype]]屬性),根據ECMA,其中[[Prototype]]屬性不可見、不可枚舉
2、建立一個函數(有name、prototype屬性),再透過prototype屬性引用剛才建立的物件
3、建立變數A,同時把函數的引用賦值給變數A
如下圖:
(注意圖中都是「 引用」類型)
每個函數的創建都經歷上述過程。
二、建構子
那麼什麼是建構子呢?
依照ECMA的定義
Constructor is a function that creates and initializes the newly created object.
構造函數是用來新建同時初始化一個新物件的函式。
什麼樣的函數可以用來建立同時初始化新物件呢?答案是:任何一個函數,包括空函數。
所以,結論是:任何一個函數都可以是建構子。
三、原型
根據前面空函數的創建圖示,我們知道每個函數在創建的時候都自動添加了prototype屬性,這就是函數的原型,從圖中可知其實質就是對一個對象的引用(這個物件暫且取名原型物件)。
我們可以對函數的原型物件進行操作,和普通的物件無異!一起來證實一下。
圍繞剛才建立的空函數,這次為空函數增加一些程式碼:
function A() {
this.width = 10;
this.data = [1,2,3];
this.key = "this this.data = [1,2,3];
this.key = "this is A";
}
A._objectNum = 0;//定義A的屬性
A.prototype.say = function(){//給A的原型物件新增屬性
alert("helloworld")
第7~9行程式碼就是增加函數的原型物件一個say屬性並引用一個匿名函數,根據「函數創建」過程,圖解如下:
(灰色背景就是在空函數基礎上增加的屬性)
簡單說原型就是函數的屬性,在函數的建立過程中由js編譯器自動加入。
那麼原型有什麼用呢?
先了解下new運算符,如下:
1
2 var a1 = new A;
var a2 = new A;
這是透過建構函式來建立物件的方式,那麼建立物件為什麼要這樣建立而不是直接var a1 = {};呢?這就涉及new的具體步驟了,這裡的new操作可以分成三步(以a1的創建為例):
1、新建一個對象並賦值給變量a1:var a1 = {};
2、把這個對象的[[Prototype]]屬性指向函數A的原型物件:a1.[[Prototype]] = A.prototype
3、呼叫函數A,同時把this指向1中建立的物件a1,對物件進行初始化:A. apply(a1,arguments)
其結構圖示如下:
從圖中看到,無論是物件a1還是a2,都有一個屬性保存了對函數A的原型物件的引用,對於這些物件來說,一些公用的方法可以在函數的原型中找到,節省了內存空間。
四、原型鏈
了解了new運算子以及原型的作用之後,一起來看看什麼是[[Prototype]]?以及物件如何沿著這個引用來進行屬性的尋找?
在js的世界裡,每個物件預設都有一個[[Prototype]]屬性,其保存著的位址就構成了物件的原型鏈,它是由js編譯器在物件被創建的時候自動添加的,其取值由new運算子的右側參數決定:當我們var object1 = {};的時候,object1的[[Prototype]]就指向Object構造函數的原型對象,因為var object1 = {};實質上等於var object = new Object();(原因可參考上述對new A的分析過程)。
物件在尋找某個屬性的時候,會先遍歷自身的屬性,如果沒有則會繼續找[[Prototype]]引用的對象,如果再沒有則繼續找[[Prototype]].[[Prototype]]引用的對象,依序類推,直到[[Prototype]].….[[Prototype]]為undefined(Object的[[Prototype]]就是undefined)
如上圖所示:
//我們想要取得a1.fGetName
alert(a1.fGetName);//輸出undefined
//1、遍歷a1物件本身 www.2cto.com
//結果a1物件本身沒有fGetName屬性
//2、找到a1的[[Prototype]],也就是其對應的物件A.prototype,同時進行遍歷
//結果A.prototype也沒有這個屬性
//3、找到A.prototype物件的[[Prototype]],指向其對應的物件Object.prototype
/ /結果Object.prototype也沒有fGetName
//4、試圖尋找Object.prototype的[[Prototype]]屬性,結果回傳undefined,這就是a1.fGetName的值
簡單說就是透過物件的[[Prototype]]儲存對另一個物件的引用,透過這個引用往上進行屬性的查找,這就是原型鏈。
五、繼承
有了原型鏈的概念,就可以進行繼承。
1 function B() {};
這個時候產生了B的原型B.prototype
原型本身就是一個Object對象,我們可以看看裡面放著哪些資料
B.prototype 其實就是{constructor : B , [ [Prototype]] : Object.prototype}
因為prototype本身是一個Object物件的實例,所以其原型鏈指向的是Object的原型
B.prototype = A.prototype;//相當於把B的prototype指向了A的prototype;這樣只是繼承了A的prototype方法,A中的自訂方法則不繼承
B.prototype.thisisb = "this is constructor B";//這樣也會改變a的prototype
但是我們只想把B的原型鏈指向A,如何實現?
第一種是透過改變原型鏈引用地址
1 B.prototype.__proto__ = A.prototype;
ECMA中並沒有__proto__這個方法,這個是ff、chrome等js解釋器添加的,等同於EMCA的[[Prototype]],這不是標準方法,那麼如何運用標準方法呢?
我們知道new操作的時候,實際上只是把實例物件的原型鏈指向了建構函數的prototype位址區塊,那麼我們可以這樣操作
1 B.prototype = new A();
這樣產生的結果是:
產生一個A的實例,同時賦值給B的原型,也即B.prototype 相當於物件{width :10 , data : [1,2,3] , key : "this is A" , [[Prototype]] : A.prototype}
這樣就把A的原型透過B.prototype.[[Prototype]]這個物件屬性保存起來,構成了原型的連結
但是注意,這樣B產生的物件的建構子發生了改變,因為在B中沒有constructor屬性,只能從原型鏈找到A.prototype,讀出constructor:A
var b = new B;
console.log(b.constructor);//output A
所以我們還要人為設回B本身
B.prototype.constructor = B;
//現在B的原型就變成了{width :10 , data : [1,2,3] , key : "this is A" , [[Prototype]] : A.prototype , constructor : B}
console.log(b.constructor);//output B
//同時B直接透過原型繼承了A的自訂屬性width和name
console.log(b.data) ;//output [1,2,3]
//這樣的壞處就是
b.data.push(4);//直接改變了prototype的data陣列(引用)
var c = new B;
alert( c.data);//output [1,2,3,4]
//其實我們想要的只是原型鏈,A的自訂屬性我們想要在B中定義(而不是在prototype)
//該如何進行繼承?
//既然我們不要A中自訂的屬性,那麼可以想辦法把其過濾掉
//可以新建一個空函數
function F(){}
//把空函數的原型指向建構子A的原型
F.prototype = A.prototype;
//這時候再透過new操作把B.prototype的原型鏈指向F的原型
B.prototype = new F;
//這時候B的原型變成了{[[Prototype]] : F.prototype}
//這裡F.prototype其實只是一個地址的引用
//但是由B創建的實例其constructor指向了A,所以這裡要顯示設定一下B.prototype的constructor屬性
B.prototype.constructor = B;
//這時候B的原型變成了{constructor : B , [[Prototype]] : F.prototype} //這樣就實現了B對A的原型繼承
圖示如下,其中紅色部分代表原型鏈: