本文譯自:JavaScript. The Core. - Dmitry Soshnikov
對(duì)象
ECMAScript 是一門(mén)高度抽象化的面向?qū)ο笳Z(yǔ)言,主要和對(duì)象打交道掏湾。雖然也有原始值,但是當(dāng)需要的時(shí)候也會(huì)被轉(zhuǎn)換為對(duì)象肿嘲。
對(duì)象是一個(gè)由屬性組成的集合融击,且有單一的原型。它的原型要么是一個(gè)對(duì)象,要么是
null
墓卦。
我們來(lái)看一個(gè)簡(jiǎn)單的對(duì)象示例枚粘。一個(gè)對(duì)象的原型由內(nèi)部的 [[Prototype]]
屬性引用。但是在用戶(hù)級(jí)的代碼中际长,我們用 __proto__
來(lái)實(shí)現(xiàn)該引用,可以讀作 'dunder proto' 兴泥。
var foo = {
x: 10,
y: 20
}
我們會(huì)得到這樣一種結(jié)構(gòu)工育,它有兩個(gè)顯式的自有屬性 x
,y
搓彻。還有一個(gè)隱式的 __proto__
屬性如绸,指向 foo
的原型。
原型有什么用旭贬?我們用原型鏈的概念來(lái)回答這個(gè)問(wèn)題怔接。
原型鏈
原型其實(shí)就是帶有自有屬性的對(duì)象。原型A指向它自身的原型——原型B稀轨,原型B再指向自身的原型——原型C扼脐,直到最終指向的原型為 null
。這就稱(chēng)為原型鏈奋刽。
原型鏈?zhǔn)且粋€(gè)由對(duì)象組成的有限鏈瓦侮,用來(lái)實(shí)現(xiàn)繼承和共享屬性。
假設(shè)我們有兩個(gè)對(duì)象佣谐,它們只有很小一部分有區(qū)別肚吏,其余的部分都一樣。顯然狭魂,一個(gè)設(shè)計(jì)良好的系統(tǒng)會(huì)重用相似的功能/代碼罚攀,而不是在每一個(gè)對(duì)象中重復(fù)一遍。在基于類(lèi)的語(yǔ)言中雌澄,這種代碼重用的語(yǔ)式稱(chēng)為基于類(lèi)的繼承——把相似的功能放入類(lèi) A
斋泄,再創(chuàng)建出繼承自 A
且擁有自身額外小改動(dòng)的 B
和 C
。
ECMAScript 沒(méi)有類(lèi)的概念掷伙。不過(guò)代碼重用的語(yǔ)式?jīng)]有太大區(qū)別(在有些方面甚至比類(lèi)更加靈活)是己,通過(guò)原型鏈就可以實(shí)現(xiàn)。這種繼承方式稱(chēng)作委托繼承(或者更接近 ECMAScript 的說(shuō)法是任柜,原型繼承)卒废。
和類(lèi) A
沛厨,B
,C
的例子相似摔认,在 ECMAScript 中逆皮,你會(huì)創(chuàng)建對(duì)象 a
, b
, c
。對(duì)象 a
中存放對(duì)象 b
和 c
的相同部分参袱,b
和 c
中只存放它們自身的額外屬性或方法电谣。
var a = {
x: 10,
calculate: function (z) {
return this.x + this.y + z
}
}
var b = {
y: 20,
__proto__: a
}
var c = {
y: 30,
__proto__: a
}
// 調(diào)用繼承方法
b.calculate(30) // 60
c.calculate(40) // 80
很簡(jiǎn)單對(duì)吧?我們可以看到 b
和 c
都能訪(fǎng)問(wèn)對(duì)象 a
中定義的 calculate
方法抹蚀。這正是通過(guò)原型鏈來(lái)實(shí)現(xiàn)的剿牺。
原理很簡(jiǎn)單:如果一個(gè)屬性或方法在對(duì)象自身中無(wú)法找到(比如對(duì)象沒(méi)有自有屬性),那么就嘗試在原型鏈中尋找該屬性/方法环壤。如果在對(duì)象的原型中也找不到該屬性晒来,那么就在原型的原型中找,如此往復(fù)郑现,直到遍歷整個(gè)原型鏈(與基于類(lèi)的繼承做法完全一樣湃崩,當(dāng)解析一個(gè)繼承方法時(shí)——我們也會(huì)找遍類(lèi)型鏈)。第一個(gè)找到的同名屬性/方法將被引用接箫。找到的這個(gè)屬性稱(chēng)作繼承屬性攒读。如果在整個(gè)原型鏈中都找不到這個(gè)屬性,則返回 undefined
辛友。
注意薄扁,在調(diào)用繼承方法時(shí),其中的 this
綁定的是調(diào)用該方法的原始對(duì)象而不是該方法所在的原型對(duì)象废累。在上面的示例中 this.y
的值取自對(duì)象 b
和 c
泌辫,而不是 a
。不過(guò) this.x
的值取自 a
九默,同樣是通過(guò)原型鏈機(jī)制。
如果一個(gè)對(duì)象沒(méi)有明確的指定其原型宾毒,則其 __proto__
默認(rèn)指向原型 Object.prototype
驼修。
原型 Object.prototype
自身也有 __proto__
屬性,它指向原型鏈的最后一環(huán) null
诈铛。
下圖展示了對(duì)象 a
乙各,b
和 c
的繼承結(jié)構(gòu)。
注意:
-
ES5中制定了另外一種原型繼承的方法幢竹,使用
Object.create
函數(shù):var b = Object.create(a, {y: {value: 20}}) var c = Object.create(a, {y: {value: 30}})
你可以在這一章中獲取更多關(guān)于 ES5 API 的信息耳峦。
ES6 已經(jīng)將
__proto__
納入標(biāo)準(zhǔn),它可以用于對(duì)象的初始化焕毫。
我們經(jīng)常會(huì)需要用到一些有相同或相似聲明結(jié)構(gòu)(比如相同屬性)但聲明值不同的對(duì)象蹲坷。這種情況我們可以使用構(gòu)造函數(shù)驶乾,它能用特定的格式創(chuàng)建對(duì)象。
構(gòu)造函數(shù)
除了用特定格式創(chuàng)建對(duì)象循签,構(gòu)造函數(shù)還有一個(gè)重要的作用 —— 它會(huì)為新創(chuàng)建的對(duì)象自動(dòng)指定一個(gè)原型级乐。這個(gè)原型就存放在 ConstructorFunction.prototype
屬性里。
我們可以用構(gòu)造函數(shù)重寫(xiě)前面例子中的對(duì)象 b
和 c
县匠。這樣风科,Foo.prototype
就扮演了對(duì)象 a
的角色:
// 一個(gè)構(gòu)造函數(shù)
function Foo(y) {
// 可以用固定格式創(chuàng)建對(duì)象:
// 他們有后生成的自有 'y' 屬性
this.y = y
}
// 同時(shí) "Foo.prototype" 里存放著新創(chuàng)建對(duì)象的原型的引用,
// 所以我們可以用它來(lái)定義共享的/繼承的屬性或方法,于是和前面例子一樣乞旦,我們創(chuàng)建:
// 繼承屬性 "x"
Foo.prototype.x = 10
// 還有繼承方法 "calculate"
Foo.prototype.calculate = function (z) {
return this.x + this.y + z
}
// 再來(lái)用“模板” Foo 創(chuàng)建對(duì)象 "b" 和 "c"
var b = new Foo(20)
var c = new Foo(30)
// 調(diào)用繼承方法
b.calculate(30) // 60
c.calculate(40) // 80
// 來(lái)看看屬性引用是否和預(yù)期的一樣
console.log(
b.__proto__ === Foo.prototype, // true
c.__proto__ === Foo.prototype, // true
// 同時(shí) "Foo.prototype" 自動(dòng)創(chuàng)建一個(gè)特殊屬性 "constructor" 贼穆,
// 指向構(gòu)造函數(shù)本身;
// 實(shí)例對(duì)象 "b" 和 "c" 可以透過(guò)委托找到該屬性并且用它來(lái)查看它們的構(gòu)造器兰粉。
b.constructor === Foo, // true
c.constructor === Foo, // true
Foo.prototype.constructor === Foo, // true
b.calculate === b.__proto__.calculate, // true
b.__proto__.calculate === Foo.prototype.calculate // true
)
這段代碼可以用下圖的關(guān)系來(lái)表達(dá):
這張圖再次展示了每一個(gè)對(duì)象都有原型故痊。構(gòu)造函數(shù) Foo
也有自己的 __proto__
,它指向 Function.prototype
亲桦,而 Function.prototype
又通過(guò) __proto__
指向 Object.prototype
崖蜜。`
Foo.prototype
就是 Foo
的一個(gè)顯式屬性。它是對(duì)象 b
和 c
的原型客峭。
嚴(yán)格來(lái)說(shuō)豫领,如果要分類(lèi)的話(huà),構(gòu)造函數(shù)和原型的組合可以稱(chēng)作“類(lèi)”舔琅。實(shí)際上等恐,像 Python 的頭等動(dòng)態(tài)類(lèi)的實(shí)現(xiàn)和屬性/方法這種解決方案完全一樣。由此看來(lái)备蚓,Python 中的類(lèi)其實(shí)是 ECMAScript 委托繼承的一種語(yǔ)法糖课蔬。
注意:
- 在 ES6 中,類(lèi) “class” 的概念已經(jīng)納入標(biāo)準(zhǔn)郊尝,作為上面所述的構(gòu)造函數(shù)的語(yǔ)法糖二跋。由此看來(lái),原型鏈成為了類(lèi)繼承的一種詳細(xì)實(shí)現(xiàn)流昏。
// ES6
class Foo {
constructor(name) {
this._name = name;
}
getName() {
return this._name;
}
}
class Bar extends Foo {
getName() {
return super.getName() + ' Doe';
}
}
var bar = new Bar('John');
console.log(bar.getName()); // John Doe
在 ES3 系列文章的第7章中可以找到這部分內(nèi)容更完整和詳細(xì)的解析扎即。其中分為兩個(gè)部分:7.1.面向?qū)ο缶幊蹋焊耪?/a>,在這部分中你可以找到各種面向?qū)ο缶幊痰姆独驼Z(yǔ)式况凉,以及它們和 ECMAScript 的對(duì)比谚鄙,還有 7.2.面向?qū)ο缶幊蹋篍CMAScript 實(shí)現(xiàn),完全忠于 ECMAScript 中的面向?qū)ο缶幊虒?shí)現(xiàn)刁绒。
現(xiàn)在我們已經(jīng)了解了對(duì)象的基本面闷营,繼續(xù)來(lái)看運(yùn)行時(shí)程序執(zhí)行在 ECMAScript 中如何實(shí)現(xiàn)。這就是所謂的一個(gè)執(zhí)行上下文棧知市,其中的每一個(gè)元素都可以抽象地用對(duì)象來(lái)代表傻盟。沒(méi)錯(cuò)速蕊,ECMAScript 中幾乎所有地方都用對(duì)象的概念運(yùn)作。