前幾天看了《再談js面向對象編程》,當時就請教哈大神,發現文章有的地方可能會造成誤導(或者說和ECMA有出入),後來自己翻一翻ECMA,總算找到“標準”的理解……
本文適合初學者,特別是對構造函數、原型和原型鏈概念比較模糊的,大牛請路過,好了,讓我們一步步來看看js的原型(鏈)到底有多神祕……
一、函數創建過程
在瞭解原型鏈之前我們先來看看一個函數在創建過程中做了哪些事情,舉一個空函數的例子:
當我們在代碼裏面聲明這麼一個空函數,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屬性,這就是函數的原型,從圖中可知其實質就是對一個對象的引用(這個對象暫且取名原型對象)。
我們可以對函數的原型對象進行操作,和普通的對象無異!一起來證實一下。
圍繞剛纔創建的空函數,這次給空函數增加一些代碼:
A.prototype.say
= function (){ |
第7~9行代碼就是給函數的原型對象增加一個say屬性並引用一個匿名函數,根據“函數創建”過程,圖解如下:
(灰色背景就是在空函數基礎上增加的屬性)
簡單說原型就是函數的一個屬性,在函數的創建過程中由js編譯器自動添加。
那麼原型有什麼用呢?
先了解下new運算符,如下:
這是通過構造函數來創建對象的方式,那麼創建對象爲什麼要這樣創建而不是直接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
)
如上圖所示:
簡單說就是通過對象的[[Prototype]]保存對另一個對象的引用,通過這個引用往上進行屬性的查找,這就是原型鏈。
五、繼承
有了原型鏈的概念,就可以進行繼承。
這個時候產生了B的原型B.prototype
原型本身就是一個Object對象,我們可以看看裏面放着哪些數據
B.prototype 實際上就是 {constructor : B , [[Prototype]] : Object.prototype}
因爲prototype本身是一個Object對象的實例,所以其原型鏈指向的是Object的原型
B.prototype
= A.prototype; |
B.prototype.thisisb
= "this
is constructor B" ; |
但是我們只想把B的原型鏈指向A,如何實現?
第一種是通過改變原型鏈引用地址
B.prototype.__proto__
= A.prototype; |
ECMA中並沒有__proto__這個方法,這個是ff、chrome等js解釋器添加的,等同於EMCA的[[Prototype]],這不是標準方法,那麼如何運用標準方法呢?
我們知道new操作的時候,實際上只是把實例對象的原型鏈指向了構造函數的prototype地址塊,那麼我們可以這樣操作
這樣產生的結果是:
產生一個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
console.log(b.constructor); |
所以我們還要人爲設回B本身
B.prototype.constructor
= B; |
console.log(b.constructor); |
F.prototype
= A.prototype; |
B.prototype.constructor
= B; |
圖示如下,其中紅色部分代表原型鏈: