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)置屬性,被一些瀏覽器自己通過__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é),我們很快可以畫出這幾行代碼所做的事情:

這樣 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)系,所以畫出的圖也很純粹:

這種方式的特點(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)系圖有了一些變化:

我們可以從圖中看到,實(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í)例共享
- 無法向父類構(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),parent 和 child 中都有一份 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')
例圖如下:

這種方式的高效率體現(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)容。