Prototype
與 __proto__
我們先寫下一行代碼:
function Parent {}
當(dāng)我們寫下這簡(jiǎn)單的一行代碼時(shí)亏拉,實(shí)際上發(fā)生了兩件事情
- 創(chuàng)建了一個(gè)構(gòu)造函數(shù)
Parent
- 創(chuàng)建了一個(gè)原型對(duì)象
prototype
如下圖:
構(gòu)造函數(shù) Parent
中 有一個(gè) prototype
的屬性指向 Parent
的 原型對(duì)象 prototype
原型對(duì)象 prototype
則有一個(gè) constructor
的屬性 指向回 構(gòu)造函數(shù) Parent
緊接著娇豫,我們又寫下一行代碼:
var parent = new Parent()
此時(shí)圆米,圖片上多出一個(gè)新成員
注意到圖中的 Parent
的實(shí)例 parent
里悦荒,有一個(gè)[[prototype]]
,為什么這里不是 __proto__
呢县好?
其實(shí)构哺,這里的 [[prototype]]
表示一種標(biāo)準(zhǔn)規(guī)范內(nèi)置屬性,被一些瀏覽器自己通過(guò)__proto__
實(shí)現(xiàn)了熬芜,對(duì)于 Chrome
的實(shí)現(xiàn)來(lái)說(shuō)莲镣,這個(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__()
* }
*/
我們之所以能通過(guò) parent.__proto__
訪問(wèn)到瑞侮,是因?yàn)橥ㄟ^(guò)原型鏈訪問(wè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é)季俩,我們很快可以畫出這幾行代碼所做的事情:
這樣 child
就可以通過(guò)原型鏈繼承的方式訪問(wèn)到 parent
以及 Parent.prototype
上的屬性和方法了慈缔。 這種方式的特點(diǎn)是:
- 引用類型的屬性為所有實(shí)例共享
- 無(wú)法向父類構(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')
可以看到,這種方式和 原型 沒(méi)有任何關(guān)系种玛,所以畫出的圖也很純粹:
這種方式的特點(diǎn)是:
- 每個(gè)實(shí)例上的屬性都是獨(dú)立的
- 可以向父類構(gòu)造函數(shù)傳參
- 每次創(chuàng)建實(shí)例都會(huì)創(chuàng)建一遍方法
組合繼承
顧名思義藐鹤,就是講上述兩種繼承方式有機(jī)結(jié)合,通過(guò)將方法定義在 prototype
中赂韵,屬性通過(guò)借用構(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)系圖有了一些變化:
我們可以從圖中看到祭示,實(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ì)象的原型掰担。
和 原型鏈繼承
對(duì)比一下,我們發(fā)現(xiàn)其實(shí)是一樣的怒炸,除了可以不用創(chuàng)建一個(gè)自定義構(gòu)造函數(shù) Child
带饱。所以特點(diǎn)和 原型鏈繼承
相同:
- 引用類型的屬性為所有實(shí)例共享
- 無(wú)法向父類構(gòu)造函數(shù)傳值
寄生式繼承
在 原型式繼承
的基礎(chǔ)上,創(chuàng)建一個(gè)僅用于封裝繼承過(guò)程的函數(shù)阅羹,該函數(shù)在內(nèi)部以某種形式來(lá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)
通過(guò)增強(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)梳虽,parent
和 child
中都有一份 name
屬性。
因此灾茁,通過(guò) 在 寄生組合式繼承
中的 createObject
方法窜觉,間接的讓 Child.prototype
訪問(wèn)到 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')
例圖如下:
這種方式的高效率體現(xiàn)它只調(diào)用了一次 Parent 構(gòu)造函數(shù)禀挫,并且因此避免了在 Parent.prototype 上面創(chuàng)建不必要的、多余的屬性拓颓。與此同時(shí)语婴,原型鏈還能保持不變;因此驶睦,還能夠正常使用 instanceof 和 isPrototypeOf砰左。開(kāi)發(fā)人員普遍認(rèn)為寄生組合式繼承是引用類型最理想的繼承范式。
后記
從 Prototype 開(kāi)始說(shuō)起 一共分為兩篇场航,從兩個(gè)角度來(lái)講述 JavaScript 原型相關(guān)的內(nèi)容缠导。