從 Prototype 開始提及(上)—— 圖解 ES5 繼承相關

Prototype__proto__

咱們先寫下一行代碼:javascript

function Parent {}

當咱們寫下這簡單的一行代碼時,實際上發生了兩件事情java

  • 建立了一個構造函數 Parent
  • 建立了一個原型對象 prototype

以下圖:segmentfault

QQ20191116-200718@2x.png

構造函數 Parent 中 有一個 prototype 的屬性指向 Parent 的 原型對象 prototype
原型對象 prototype 則有一個 constructor 的屬性 指向回 構造函數 Parent 瀏覽器

緊接着,咱們又寫下一行代碼:函數

var parent = new Parent()

此時,圖片上多出一個新成員this

QQ20191116-201047@2x.png

注意到圖中的 Parent 的實例 parent 裏,有一個[[prototype]],爲何這裏不是 __proto__呢?spa

其實,這裏的 [[prototype]] 表示一種標準規範內置屬性,被一些瀏覽器本身經過__proto__實現了,對於 Chrome 的實現來講,這個 __proto__ 也並不存在於 實例 parent中,而是 Object.prototype 的一個 存取描述符,如下代碼能夠證實:prototype

parent.hasOwnProperty('__proto__') // false

Object.prototype.hasOwnProperty('__proto__') // true

Object.getOwnPropertyDescriptor(Object.prototype, '__proto__')
/**
 * {
 *    configurable: true,
 *    enumerable: false,
 *    get: f __proto__()
 *    set: f __proto__()
 * }
 */

咱們之因此能經過 parent.__proto__ 訪問到,是由於經過原型鏈訪問到了 Object.prototype 上的 __proto__ 存取描述符。設計

ES5 的 6 種繼承

如下內容更像是《JavaScript高級程序設計》的筆記,主要提煉出每一個繼承的特色以及例圖。code

原型鏈繼承

function Parent() {}
function Child() {}

var parent = new Parent()
Child.prototype = parent

var child = new Child()

此時,根據第一部分所描述的細節,咱們很快能夠畫出這幾行代碼所作的事情:

QQ20191116-204120@2x.png

這樣 child 就能夠經過原型鏈繼承的方式訪問到 parent 以及 Parent.prototype 上的屬性和方法了。 這種方式的特色是:

  • 引用類型的屬性爲全部實例共享
  • 沒法向父類構造函數傳值

借用構造函數繼承(經典繼承)

function Parent(name){
    this.name = name
}
function Child(name){
    Parent.call(this, name)
}

var child1 = new Child('child1')
var child2 = new Child('child2')

能夠看到,這種方式和 原型 沒有任何關係,因此畫出的圖也很純粹:

QQ20191116-210140@2x.png

這種方式的特色是:

  • 每一個實例上的屬性都是獨立的
  • 能夠向父類構造函數傳參
  • 每次建立實例都會建立一遍方法

組合繼承

顧名思義,就是講上述兩種繼承方式有機結合,經過將方法定義在 prototype 中,屬性經過借用構造函數繼承的方式實現繼承。

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

Parent.prototype.talk = function () {}

function Child(name) {
    Parent.call(this, name)
}

var parent = new Parent('parent')
Child.prototype = parent
Child.prototype.constructor = Child

var child = new Child('child')

此時,關係圖有了一些變化:

QQ20191117-202056.png

咱們能夠從圖中看到,實例 child 和 實例 parent 各自擁有獨立的 namne,可是共享 Parent.prototype 中的 talk() 方法。這種方式的特色是:

  • 擁有以上兩種方式的優勢
  • 執行了兩次 父類構造函數 Parent

原型式繼承

function createObject(o) {
    function F() {}
    F.prototype = o
    return new F()
}

function Parent() {}

var parent = new Parent()

var child = object(parent)

這裏先建立了一個 createObject 函數,其實就是 ES5 Object.create 的模擬實現,將傳入的對象做爲建立的對象的原型。

QQ20191116-212737@2x.png

原型鏈繼承 對比一下,咱們發現實際上是同樣的,除了能夠不用建立一個自定義構造函數 Child。因此特色和 原型鏈繼承 相同:

  • 引用類型的屬性爲全部實例共享
  • 沒法向父類構造函數傳值

寄生式繼承

原型式繼承 的基礎上,建立一個僅用於封裝繼承過程的函數,該函數在內部以某種形式來作加強對象,最後返回對象。

function createObject(o) {
    function F() {}
    F.prototype = o
    return new F()
}

function enhanceObject(o) {
    var clone = createObject(o)
    clone.talk = function() {}
    return clone
}

function Parent() {}

var parent = new Parent()

var child = enhanceObject(parent)

經過加強對象,每次建立的新實例,所擁有的方法不是共享 Parent.prototype 中的,而是各自獨立建立的。所以,該方式的特色相似借用構造函數繼承

  • 可添加函數,加強能力
  • 每次建立對象都會建立一遍方法

寄生組合式繼承

咱們在 組合繼承 中發現,這種方式最大的缺點是會調用兩次父構造函數,
一次是設置子類型實例的原型的時候:

var parent = new Parent('parent')
Child.prototype = parent

一次在建立子類型實例的時候:

var child = new Child('child')

回想下 new 的模擬實現,其實在這句中,咱們會執行:

Parent.call(this, name)

因此咱們在例圖中能夠發現,parentchild 中都有一份 name 屬性。

所以,經過 在 寄生組合式繼承 中的 createObject 方法,間接的讓 Child.prototype 訪問到 Parent.prototype,從而減小調用父構造函數的次數。

function createObject(o) {
    function F() {}
    F.prototype = o
    return new F()
}

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

function Child(name) {
    Parent.call(this, name)
}

Child.prototype = createObject(Parent.prototype)
Child.prototype.constructor = Child

var child = new Child('child')

例圖以下:

QQ20191116-220051@2x.png

這種方式的高效率體現它只調用了一次 Parent 構造函數,而且所以避免了在 Parent.prototype 上面建立沒必要要的、多餘的屬性。與此同時,原型鏈還能保持不變;所以,還可以正常使用 instanceof 和 isPrototypeOf。開發人員廣泛認爲寄生組合式繼承是引用類型最理想的繼承範式。

後記

從 Prototype 開始提及 一共分爲兩篇,從兩個角度來說述 JavaScript 原型相關的內容。

相關文章
相關標籤/搜索