《前端竹節》(3)【原型與對象】

作前端開發有段時間了,遇到過不少坎,如果要排出個前後順序,那麼JavaScript的原型與對象絕對逃不出TOP3。前端

若是說前端是海,JavaScript就是海里的水

一直以來都想寫篇文章梳理一下這塊,爲了加深本身的理解,也爲了幫助後來者儘快出坑,但總覺缺乏恰當的切入點,使讀者能看到清晰的路徑而非生硬的教科書。最近看到句話「好的問題如庖丁之刃,能幫你輕鬆剖開現象直達本質」,因此本文以層層探問解答的方式,試圖提供一個易於理解的角度。編程

如今的軟件開發,不多有不是面向對象的,那麼JavaScript如何建立對象?

1、 建立對象的方法

在傳統的面向對象編程語言(如:C++,Java等)中,都用定義類的關鍵字class,首先聲明一個類,而後再經過類實例化出對象實例。但在JavaScript中若實現這樣邏輯的對象建立,須要先定義一個表明類的構造函數,再經過new運算符執行構造函數實例化出對象。數組

  1. 對象字面量編程語言

    var object1 = { name: "object1" }
  2. 構造函數法函數

    var ClassMethod = function() {
        this.name = "Class"
    }
    var object2 = new ClassMethod()
    // 這種方式建立的對象字面量
    var object3 = new Object({ name: "object3" })

    這裏提到的new運算符,後面會詳述優化

  3. Object.create(proto)
    建立一個新對象,使用入參proto對象來提供新建立的對象的__proto__,也就入參對象時新建立對象的原型對象。this

    var Parent = { name: "Parent" }
    var object4 = Object.create(Parent)
想要明白JavaScript原型繼承的幺蛾子,勢必要搞清楚原型對象、實例對象、構造函數以及原型鏈的概念和關係,接下來我儘可能作到表述地結構清晰,言簡意賅。

2、原型繼承

暫時擱置一下原型鏈,我先講清楚其他三個概念的門門道道,若是你手邊有紙筆最好,沒有在腦中想象也不復雜。prototype

  1. 畫一個等邊三角形,從頂點順時針爲每一個角編號(1)、(2)、(3)
  2. 其中(1)旁邊標註「原型對象」,(2)構造函數,(3)實例對象
  3. 從(2)構造函數(如上節例中的ClassMethod)指向(3)實例對象(上節例中的object2)畫一條帶箭頭的線。線上註明new運算符,表示var object2 = new ClassName()
  4. 從(2)構造函數指向(1)原型對象畫一條帶箭頭的線。線上標註prototype,表示該構造函數的原型對象等於ClassName.prototype。(函數都有prototype屬性,指向它的原型對象)
  5. 從(3)實例對象指向(1)原型對象畫一條帶箭頭的線。線上標註__proto__,表示該實例對象的原型對象等於object2.__proto__,結合第4步,便有ClassName.prototype === object2.__proto__
  6. 從(1)原型對象指向(2)構造函數畫一條帶箭頭的線。線上標註constructor,表示該原型對象的構造函數等於ClassName === object2.__proto__.constructor

關於JavaScript函數與對象自帶的屬性有一句須要畫重點的話:全部的對象都有一個__proto__屬性指向其原型對象,全部的函數都有prototype屬性,指向它的原型對象。函數其實也是一種對象,那麼函數便有兩個原型對象。因爲平時更關注對象依據__proto__屬性,指向的原型對象所構成的原型鏈,爲了區分函數的兩個原型,便將__proto__所指的原型對象稱做隱式原型,而把prototype所指向的原型對象稱做顯示原型指針

看到這裏你應該已經知道原型對象、實例對象、構造函數以及原型鏈是什麼了,可是對於爲何是這樣應該還比較懵,由於我也曾如此,用以往類與對象,父類與子類的概念對照原型與實例,試圖想找出一些熟悉的關係,讓本身可以理解。code

人們老是習慣經過熟悉的事物,類比去認識陌生的事物。這或許是一種快速的方式,但這絕對不是一種有效的方式。類比總會讓咱們輕視邏輯推理

3、從instanceof再看原型鏈

語法格式爲object instanceof constructor,從字面上理解instanceof,是用來判斷object是否爲constructor構造函數實例化出的對象。但除此以外,若構造函數所指的顯示原型對象constructor.prototype存在於object的原型鏈上,結果也都會爲true

字面理解多少會有些誤差,請及時 查閱MDN文檔

原型鏈就是JavaScript相關對象之間,由__proto__屬性依次引用造成的有向關係鏈,原型對象上的屬性和方法能夠被其實例對象使用。(這種有向的父子關係鏈就具備了實現類繼承的特性)

4、new運算符

new Foo()執行過程當中,都發生了什麼?

如下三步:

  1. 建立一個繼承自Foo.prototype的新對象。
  2. 執行構造函數Foo,並將this指針綁定到新建立的對象上。
  3. 若是構造函數返回一個對象,則這個對象就是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
    }
}

5、實現繼承

在ES6中,出現了更爲直觀的語法糖形式: class Child extends Parent{},但這裏咱們只看看以前沒有這種語法糖是怎麼實現的。我一直有一個體會: 要想快速的瞭解一個事物,就去了解它的源起流變

首先定義一個父類Parent,以及它的一個屬性name:

function Parent() {
    this.name = 'parent'
}

接下來如何定義一個繼承自Parent的子類Child

  1. 構造函數方式

    function Child() {
        Parent.call(this)
        this.type = 'subClass' // ... 這裏還可定義些子類的屬性和方法
    }

    這種方式的缺陷是:父類原型鏈上的屬性和方法不會被子類繼承。

  2. 原型鏈方式

    function Child() {
        this.type = 'subClass'
    }
    Child.prototype = new Parent()

    這種方式彌補了子類無法繼承父類原型鏈上屬性和方法的缺陷,與此同時又引入一個新的問題:父類上的對象或數組屬性會引用傳遞給子類實例。
    好比父類上有一個數組屬性arr,現經過new Child()實例化出兩個實例對象c1c2,那麼c1對其arr屬性的操做同時也會引發c2.arr的改變,這固然不是咱們想要的。

  3. 組合方式(綜合1,2兩種方式)

    function Child() {
        Parent.call(this)
        this.type = 'subClass'
    }
    Child.prototype = new Parent()

    雖然解決了上述問題,但明顯看到這裏構造函數執行了兩遍,顯然有些多餘。

  4. 組合優化方式

    function Child() {
        Parent.call(this)
        this.type = 'subClass'
    }
    Child.prototype = Parent.prototype

    這種方式減小了多餘的父類構造函數調用,但子類的顯示原型會被覆蓋。此例中經過子類構造函數實例化一個對象:var cObj = new Child(),能夠驗證出實例對象的原型對象,是父類構造函數的顯示原型:cObj.__proto__.constructor === Parent,顯然這種方式依舊不很完美。

  5. 終極方式

    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獲取到。

  6. 多繼承

    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的原型。

相關文章
相關標籤/搜索