作前端開發有段時間了,遇到過不少坎,如果要排出個前後順序,那麼JavaScript的原型與對象絕對逃不出TOP3。前端
若是說前端是海,JavaScript就是海里的水
一直以來都想寫篇文章梳理一下這塊,爲了加深本身的理解,也爲了幫助後來者儘快出坑,但總覺缺乏恰當的切入點,使讀者能看到清晰的路徑而非生硬的教科書。最近看到句話「好的問題如庖丁之刃,能幫你輕鬆剖開現象直達本質」,因此本文以層層探問解答的方式,試圖提供一個易於理解的角度。編程
如今的軟件開發,不多有不是面向對象的,那麼JavaScript如何建立對象?
在傳統的面向對象編程語言(如:C++,Java等)中,都用定義類的關鍵字class
,首先聲明一個類,而後再經過類實例化出對象實例。但在JavaScript中若實現這樣邏輯的對象建立,須要先定義一個表明類的構造函數,再經過new
運算符執行構造函數實例化出對象。數組
對象字面量編程語言
var object1 = { name: "object1" }
構造函數法函數
var ClassMethod = function() { this.name = "Class" } var object2 = new ClassMethod() // 這種方式建立的對象字面量 var object3 = new Object({ name: "object3" })
這裏提到的new
運算符,後面會詳述優化
Object.create(proto)
建立一個新對象,使用入參proto
對象來提供新建立的對象的__proto__
,也就入參對象時新建立對象的原型對象。this
var Parent = { name: "Parent" } var object4 = Object.create(Parent)
想要明白JavaScript原型繼承的幺蛾子,勢必要搞清楚原型對象、實例對象、構造函數以及原型鏈的概念和關係,接下來我儘可能作到表述地結構清晰,言簡意賅。
暫時擱置一下原型鏈,我先講清楚其他三個概念的門門道道,若是你手邊有紙筆最好,沒有在腦中想象也不復雜。prototype
ClassMethod
)指向(3)實例對象(上節例中的object2
)畫一條帶箭頭的線。線上註明new
運算符,表示var object2 = new ClassName()
。prototype
,表示該構造函數的原型對象等於ClassName.prototype
。(函數都有prototype
屬性,指向它的原型對象)__proto__
,表示該實例對象的原型對象等於object2.__proto__
,結合第4步,便有ClassName.prototype === object2.__proto__
。constructor
,表示該原型對象的構造函數等於ClassName === object2.__proto__.constructor
。關於JavaScript函數與對象自帶的屬性有一句須要畫重點的話:全部的對象都有一個__proto__
屬性指向其原型對象,全部的函數都有prototype
屬性,指向它的原型對象。函數其實也是一種對象,那麼函數便有兩個原型對象。因爲平時更關注對象依據__proto__
屬性,指向的原型對象所構成的原型鏈,爲了區分函數的兩個原型,便將__proto__
所指的原型對象稱做隱式原型,而把prototype
所指向的原型對象稱做顯示原型。指針
看到這裏你應該已經知道原型對象、實例對象、構造函數以及原型鏈是什麼了,可是對於爲何是這樣應該還比較懵,由於我也曾如此,用以往類與對象,父類與子類的概念對照原型與實例,試圖想找出一些熟悉的關係,讓本身可以理解。code
人們老是習慣經過熟悉的事物,類比去認識陌生的事物。這或許是一種快速的方式,但這絕對不是一種有效的方式。類比總會讓咱們輕視邏輯推理
instanceof
再看原型鏈語法格式爲object instanceof constructor
,從字面上理解instanceof
,是用來判斷object
是否爲constructor
構造函數實例化出的對象。但除此以外,若構造函數所指的顯示原型對象constructor.prototype
存在於object
的原型鏈上,結果也都會爲true
。
字面理解多少會有些誤差,請及時 查閱MDN文檔
原型鏈就是JavaScript相關對象之間,由__proto__
屬性依次引用造成的有向關係鏈,原型對象上的屬性和方法能夠被其實例對象使用。(這種有向的父子關係鏈就具備了實現類繼承的特性)
new
運算符
new Foo()
執行過程當中,都發生了什麼?
如下三步:
Foo.prototype
的新對象。Foo
,並將this
指針綁定到新建立的對象上。new
運算符執行的結果;若是沒返回對象,則使用第一步建立出的新對象。爲了直觀的理解,這裏自定義一個函數myNew
來模擬new
運算符
function myNew(Foo){ var tmp = Object.create(Foo.prototype) var ret = Foo.call(tmp) if (typeof ret === 'object') { return ret } else { return tmp } }
在ES6中,出現了更爲直觀的語法糖形式:
class Child extends Parent{}
,但這裏咱們只看看以前沒有這種語法糖是怎麼實現的。我一直有一個體會:
要想快速的瞭解一個事物,就去了解它的源起流變。
首先定義一個父類Parent,以及它的一個屬性name:
function Parent() { this.name = 'parent' }
接下來如何定義一個繼承自Parent
的子類Child
:
構造函數方式
function Child() { Parent.call(this) this.type = 'subClass' // ... 這裏還可定義些子類的屬性和方法 }
這種方式的缺陷是:父類原型鏈上的屬性和方法不會被子類繼承。
原型鏈方式
function Child() { this.type = 'subClass' } Child.prototype = new Parent()
這種方式彌補了子類無法繼承父類原型鏈上屬性和方法的缺陷,與此同時又引入一個新的問題:父類上的對象或數組屬性會引用傳遞給子類實例。
好比父類上有一個數組屬性arr
,現經過new Child()
實例化出兩個實例對象c1
和c2
,那麼c1
對其arr
屬性的操做同時也會引發c2.arr
的改變,這固然不是咱們想要的。
組合方式(綜合1,2兩種方式)
function Child() { Parent.call(this) this.type = 'subClass' } Child.prototype = new Parent()
雖然解決了上述問題,但明顯看到這裏構造函數執行了兩遍,顯然有些多餘。
組合優化方式
function Child() { Parent.call(this) this.type = 'subClass' } Child.prototype = Parent.prototype
這種方式減小了多餘的父類構造函數調用,但子類的顯示原型會被覆蓋。此例中經過子類構造函數實例化一個對象:var cObj = new Child()
,能夠驗證出實例對象的原型對象,是父類構造函數的顯示原型:cObj.__proto__.constructor === Parent
,顯然這種方式依舊不很完美。
終極方式
function Child() { Parent.call(this) this.type = 'subClass' } Child.prototype = Object.create(Parent.prototype) Child.prototype.constructor = Child
實例對象的__proto__
屬性值老是該實例對象的構造函數的prototype
屬性。這裏關於構造函數的從屬關係存在一個易混淆的點,我多囉嗦幾句來試圖把這塊講清楚:還記的上面咱們畫的那個三角形麼?三個角分別表明構造函數、實例對象和原型對象,三條有向邊分別表明new
,__proto__
,prototype
,根據__proto__
有向邊串聯起來鏈即是原型鏈。
要解釋清楚構造函數的從屬關係,咱們先在上面所畫的原型鏈三角形中的每一個三角形中,添加一條有向邊:從原型對象指向構造函數,這表示原型對象有一個constructor
屬性指向它的構造函數,而該構造函數的prototype
屬性又指向這個構造函數,因而便在局部造成了一個有向環。
如今一切都協調了,惟獨還有一點,就是原型鏈末端的實例對象構造函數的指向,不論經過new
運算符仍是經過Object.create
建立出來的實例對象的constructor
屬性,都和其原型對象的constructor
相同。因此爲了保持一致性便有了上面那句Child.prototype.constructor = Child
,爲的是在你想要知道一個對象是由哪一個構造函數實例化出來的,能夠根據obj.__proto__.constructor
獲取到。
多繼承
function Child() { Parent1.call(this) Parent2.call(this) } Child.prototype = Object.create(Parent1.prototype) Object.assign(Child.prototype, Parent2.prototype) Child.prototype.constructor = Child
利用Obejct.assign
方法將Parent2
原型上的方法複製到Child
的原型。