從 Prototype 開始說起(上)—— 圖解 ES5 繼承相關(guān)

Prototype__proto__

我們先寫下一行代碼:

function Parent {}

當(dāng)我們寫下這簡(jiǎn)單的一行代碼時(shí),實(shí)際上發(fā)生了兩件事情

  • 創(chuàng)建了一個(gè)構(gòu)造函數(shù) Parent
  • 創(chuàng)建了一個(gè)原型對(duì)象 prototype

如下圖:

QQ20191116-200718@2x.png

構(gòu)造函數(shù) Parent 中 有一個(gè) prototype 的屬性指向 Parent 的 原型對(duì)象 prototype
原型對(duì)象 prototype 則有一個(gè) constructor 的屬性 指向回 構(gòu)造函數(shù) Parent

緊接著,我們又寫下一行代碼:

var parent = new Parent()

此時(shí),圖片上多出一個(gè)新成員

QQ20191116-201047@2x.png

注意到圖中的 Parent 的實(shí)例 parent 里,有一個(gè)[[prototype]],為什么這里不是 __proto__呢?

其實(shí),這里的 [[prototype]] 表示一種標(biāo)準(zhǔn)規(guī)范內(nèi)置屬性,被一些瀏覽器自己通過__proto__實(shí)現(xiàn)了,對(duì)于 Chrome 的實(shí)現(xiàn)來說,這個(gè) __proto__ 也并不存在于 實(shí)例 parent中,而是 Object.prototype 的一個(gè) 存取描述符,以下代碼可以證明:

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__ 訪問到,是因?yàn)橥ㄟ^原型鏈訪問到了 Object.prototype 上的 __proto__ 存取描述符。

ES5 的 6 種繼承

以下內(nèi)容更像是《JavaScript高級(jí)程序設(shè)計(jì)》的筆記,主要提煉出每個(gè)繼承的特點(diǎn)以及例圖。

原型鏈繼承

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

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

var child = new Child()

此時(shí),根據(jù)第一部分所描述的細(xì)節(jié),我們很快可以畫出這幾行代碼所做的事情:

QQ20191116-204120@2x.png

這樣 child 就可以通過原型鏈繼承的方式訪問到 parent 以及 Parent.prototype 上的屬性和方法了。 這種方式的特點(diǎn)是:

  • 引用類型的屬性為所有實(shí)例共享
  • 無法向父類構(gòu)造函數(shù)傳值

借用構(gòu)造函數(shù)繼承(經(jīng)典繼承)

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

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

可以看到,這種方式和 原型 沒有任何關(guān)系,所以畫出的圖也很純粹:

QQ20191116-210140@2x.png

這種方式的特點(diǎn)是:

  • 每個(gè)實(shí)例上的屬性都是獨(dú)立的
  • 可以向父類構(gòu)造函數(shù)傳參
  • 每次創(chuàng)建實(shí)例都會(huì)創(chuàng)建一遍方法

組合繼承

顧名思義,就是講上述兩種繼承方式有機(jī)結(jié)合,通過將方法定義在 prototype 中,屬性通過借用構(gòu)造函數(shù)繼承的方式實(shí)現(xiàn)繼承。

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')

此時(shí),關(guān)系圖有了一些變化:

QQ20191117-202056.png

我們可以從圖中看到,實(shí)例 child 和 實(shí)例 parent 各自擁有獨(dú)立的 namne,但是共享 Parent.prototype 中的 talk() 方法。這種方式的特點(diǎn)是:

  • 擁有以上兩種方式的優(yōu)點(diǎn)
  • 執(zhí)行了兩次 父類構(gòu)造函數(shù) Parent

原型式繼承

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

function Parent() {}

var parent = new Parent()

var child = object(parent)

這里先創(chuàng)建了一個(gè) createObject 函數(shù),其實(shí)就是 ES5 Object.create 的模擬實(shí)現(xiàn),將傳入的對(duì)象作為創(chuàng)建的對(duì)象的原型。

QQ20191116-212737@2x.png

原型鏈繼承 對(duì)比一下,我們發(fā)現(xiàn)其實(shí)是一樣的,除了可以不用創(chuàng)建一個(gè)自定義構(gòu)造函數(shù) Child。所以特點(diǎn)和 原型鏈繼承 相同:

  • 引用類型的屬性為所有實(shí)例共享
  • 無法向父類構(gòu)造函數(shù)傳值

寄生式繼承

原型式繼承 的基礎(chǔ)上,創(chuàng)建一個(gè)僅用于封裝繼承過程的函數(shù),該函數(shù)在內(nèi)部以某種形式來做增強(qiáng)對(duì)象,最后返回對(duì)象。

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)

通過增強(qiáng)對(duì)象,每次創(chuàng)建的新實(shí)例,所擁有的方法不是共享 Parent.prototype 中的,而是各自獨(dú)立創(chuàng)建的。因此,該方式的特點(diǎn)類似借用構(gòu)造函數(shù)繼承

  • 可添加函數(shù),增強(qiáng)能力
  • 每次創(chuàng)建對(duì)象都會(huì)創(chuàng)建一遍方法

寄生組合式繼承

我們?cè)?組合繼承 中發(fā)現(xiàn),這種方式最大的缺點(diǎn)是會(huì)調(diào)用兩次父構(gòu)造函數(shù),
一次是設(shè)置子類型實(shí)例的原型的時(shí)候:

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

一次在創(chuàng)建子類型實(shí)例的時(shí)候:

var child = new Child('child')

回想下 new 的模擬實(shí)現(xiàn),其實(shí)在這句中,我們會(huì)執(zhí)行:

Parent.call(this, name)

所以我們?cè)诶龍D中可以發(fā)現(xiàn),parentchild 中都有一份 name 屬性。

因此,通過 在 寄生組合式繼承 中的 createObject 方法,間接的讓 Child.prototype 訪問到 Parent.prototype,從而減少調(diào)用父構(gòu)造函數(shù)的次數(shù)。

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

這種方式的高效率體現(xiàn)它只調(diào)用了一次 Parent 構(gòu)造函數(shù),并且因此避免了在 Parent.prototype 上面創(chuàng)建不必要的、多余的屬性。與此同時(shí),原型鏈還能保持不變;因此,還能夠正常使用 instanceof 和 isPrototypeOf。開發(fā)人員普遍認(rèn)為寄生組合式繼承是引用類型最理想的繼承范式。

后記

從 Prototype 開始說起 一共分為兩篇,從兩個(gè)角度來講述 JavaScript 原型相關(guān)的內(nèi)容。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 什么是原型語言 只有對(duì)象,沒有類;對(duì)象繼承對(duì)象,而不是類繼承類。 “原型對(duì)象”是核心概念。原型對(duì)象是新對(duì)象的模板,...
    zhoulujun閱讀 2,441評(píng)論 0 12
  • ??面向?qū)ο螅∣bject-Oriented,OO)的語言有一個(gè)標(biāo)志,那就是它們都有類的概念,而通過類可以創(chuàng)建任意...
    霜天曉閱讀 2,252評(píng)論 0 6
  • 繼承 Javascript中繼承都基于兩種方式:1.通過原型鏈繼承,通過修改子類原型的指向,使得子類實(shí)例通過原型鏈...
    LeoCong閱讀 402評(píng)論 0 0
  • 理解 javascript 的原型鏈及繼承 以上所有的運(yùn)行結(jié)果都是 true; 三種構(gòu)造對(duì)象的方法: 通過對(duì)象字面...
    你期待的花開閱讀 1,664評(píng)論 0 3
  • 老屋, 承載了多少童年的美好, 哪怕會(huì)泥土飛揚(yáng), 外面大雨,屋里小雨滴答, 淋濕了我們的夢(mèng)鄉(xiāng)。 從前, 木窗里的昏...
    想飛的樹不如草閱讀 178評(píng)論 0 0

友情鏈接更多精彩內(nèi)容