感謝社區(qū)中各位的大力支持,譯者再次奉上一點(diǎn)點(diǎn)福利:阿里云產(chǎn)品券轿衔,享受所有官網(wǎng)優(yōu)惠沉迹,并抽取幸運(yùn)大獎(jiǎng):點(diǎn)擊這里領(lǐng)取
在第三,四章中害驹,我們幾次提到了[[Prototype]]
鏈鞭呕,但我們沒(méi)有討論它到底是什么。現(xiàn)在我們就詳細(xì)講解一下原型(prototype)宛官。
注意: 所有模擬類(lèi)拷貝行為的企圖葫松,也就是我們?cè)谇懊娴谒恼旅枋龅膬?nèi)容,稱(chēng)為各種種類(lèi)的“mixin”底洗,和我們要在本章中講解的[[Prototype]]
鏈機(jī)制完全不同腋么。
[[Prototype]]
JavaScript中的對(duì)象有一個(gè)內(nèi)部屬性,在語(yǔ)言規(guī)范中稱(chēng)為[[Prototype]]
枷恕,它只是一個(gè)其他對(duì)象的引用。幾乎所有的對(duì)象在被創(chuàng)建時(shí)谭胚,它的這個(gè)屬性都被賦予了一個(gè)非null
值徐块。
注意: 我們馬上就會(huì)看到,一個(gè)對(duì)象擁有一個(gè)空的[[Prototype]]
鏈接是 可能 的灾而,雖然這有些不尋常胡控。
考慮下面的代碼:
var myObject = {
a: 2
};
myObject.a; // 2
[[Prototype]]
引用有什么用?在第三章中旁趟,我們講解了[[Get]]
操作昼激,它會(huì)在你引用一個(gè)對(duì)象上的屬性時(shí)被調(diào)用,比如myObject.a
锡搜。對(duì)于默認(rèn)的[[Get]]
操作來(lái)說(shuō)橙困,第一步就是檢查對(duì)象本身是否擁有一個(gè)a
屬性,如果有耕餐,就使用它凡傅。
注意: ES6的代理(Proxy)超出了我們要在本書(shū)內(nèi)討論的范圍(將會(huì)在本系列的后續(xù)書(shū)目中涵蓋!)肠缔,但是如果加入Proxy
夏跷,我們?cè)谶@里討論的關(guān)于普通[[Get]]
和[[Put]]
的行為都是不被采用的哼转。
但是如果myObject
上 不 存在a
屬性時(shí),我們就將注意力轉(zhuǎn)向?qū)ο蟮?code>[[Prototype]]鏈槽华。
如果默認(rèn)的[[Get]]
操作不能直接在對(duì)象上找到被請(qǐng)求的屬性壹蔓,那么會(huì)沿著對(duì)象的[[Prototype]]
鏈 繼續(xù)處理。
var anotherObject = {
a: 2
};
// 創(chuàng)建一個(gè)鏈接到`anotherObject`的對(duì)象
var myObject = Object.create( anotherObject );
myObject.a; // 2
注意: 我們馬上就會(huì)解釋Object.create(..)
是做什么猫态,如何做的佣蓉。眼下先假設(shè),它創(chuàng)建了一個(gè)對(duì)象懂鸵,這個(gè)對(duì)象帶有一個(gè)鏈到指定的對(duì)象的[[Prototype]]
鏈接偏螺,這個(gè)鏈接就是我們要講解的。
那么匆光,我們現(xiàn)在讓myObject``[[Prototype]]
鏈到了anotherObject
套像。雖然很明顯myObject.a
實(shí)際上不存在,但是無(wú)論如何屬性訪問(wèn)成功了(在anotherObject
中找到了)终息,而且確實(shí)找到了值2
夺巩。
但是,如果在anotherObject
上也沒(méi)有找到a
周崭,而且如果它的[[Prototype]]
鏈不為空柳譬,就沿著它繼續(xù)查找。
這個(gè)處理持續(xù)進(jìn)行续镇,直到找到名稱(chēng)匹配的屬性美澳,或者[[Prototype]]
鏈終結(jié)。如果在鏈條的末尾都沒(méi)有找到匹配的屬性摸航,那么[[Get]]
操作的返回結(jié)果為undefined
制跟。
和這種[[Prototype]]
鏈查詢(xún)處理相似,如果你使用for..in
循環(huán)迭代一個(gè)對(duì)象酱虎,所有在它的鏈條上可以到達(dá)的(并且是enumerable
——見(jiàn)第三章)屬性都會(huì)被枚舉雨膨。如果你使用in
操作符來(lái)測(cè)試一個(gè)屬性在一個(gè)對(duì)象上的存在性,in
將會(huì)檢查對(duì)象的整個(gè)鏈條(不管 可枚舉性)读串。
var anotherObject = {
a: 2
};
// 創(chuàng)建一個(gè)鏈接到`anotherObject`的對(duì)象
var myObject = Object.create( anotherObject );
for (var k in myObject) {
console.log("found: " + k);
}
// 找到: a
("a" in myObject); // true
所以聊记,當(dāng)你以各種方式進(jìn)行屬性查詢(xún)時(shí),[[Prototype]]
鏈就會(huì)一個(gè)鏈接一個(gè)鏈接地被查詢(xún)恢暖。一旦找到屬性或者鏈條終結(jié)排监,這種查詢(xún)會(huì)就會(huì)停止。
Object.prototype
但是[[Prototype]]
鏈到底在 哪里 “終結(jié)”杰捂?
每個(gè) 普通 的[[Prototype]]
鏈的最頂端社露,是內(nèi)建的Object.prototype
。這個(gè)對(duì)象包含各種在整個(gè)JS中被使用的共通工具琼娘,因?yàn)镴avaScript中所有普通(內(nèi)建峭弟,而非被宿主環(huán)境擴(kuò)展的)的對(duì)象都“衍生自”(也就是附鸽,使它們的[[Prototype]]
頂端為)Object.prototype
對(duì)象。
你會(huì)在這里發(fā)現(xiàn)一些你可能很熟悉的工具瞒瘸,比如.toString()
和.valueOf()
坷备。在第三章中,我們介紹了另一個(gè):.hasOwnProperty(..)
情臭。還有另外一個(gè)你可能不太熟悉省撑,但我們將在這一章里討論的Object.prototype
上的函數(shù)是.isPrototypeOf(..)
。
設(shè)置與遮蔽屬性
回到第三章俯在,我們提到過(guò)在對(duì)象上設(shè)置屬性要比僅僅在對(duì)象上添加新屬性或改變既存屬性的值更加微妙【癸現(xiàn)在我們將更完整地重溫這個(gè)話題。
myObject.foo = "bar";
如果myObject
對(duì)象已直接經(jīng)擁有了普通的名為foo
的數(shù)據(jù)訪問(wèn)器屬性跷乐,那么這個(gè)賦值就和改變既存屬性的值一樣簡(jiǎn)單肥败。
如果foo
還沒(méi)有直接存在于myObject
,[[Prototype]]
就會(huì)被遍歷愕提,就像[[Get]]
操作那樣馒稍。如果在鏈條的任何地方都沒(méi)有找到foo
,那么就會(huì)像我們期望的那樣浅侨,屬性foo
就以指定的值被直接添加到myObject
上纽谒。
然而,如果foo
已經(jīng)存在于鏈條更高層的某處如输,myObject.foo = "bar"
賦值就可能會(huì)發(fā)生微妙的(也許令人詫異的)行為鼓黔。我們一會(huì)兒就詳細(xì)講解。
如果屬性名foo
同時(shí)存在于myObject
本身和從myObject
開(kāi)始的[[Prototype]]
鏈的更高層不见,這樣的情況稱(chēng)為 遮蔽澳化。直接存在于myObject
上的foo
屬性會(huì) 遮蔽 任何出現(xiàn)在鏈條高層的foo
屬性,因?yàn)?code>myObject.foo查詢(xún)總是在尋找鏈條最底層的foo
屬性脖祈。
正如我們被暗示的那樣肆捕,在myObject
上的foo
遮蔽沒(méi)有看起來(lái)那么簡(jiǎn)單刷晋。我們現(xiàn)在來(lái)考察myObject.foo = "bar"
賦值的三種場(chǎng)景盖高,當(dāng)foo
不直接存在 于myObject
,但 存在 于myObject
的[[Prototype]]
鏈的更高層:
- 如果一個(gè)普通的名為
foo
的數(shù)據(jù)訪問(wèn)屬性在[[Prototype]]
鏈的高層某處被找到眼虱,而且沒(méi)有被標(biāo)記為只讀(writable:false
)喻奥,那么一個(gè)名為foo
的新屬性就直接添加到myObject
上,形成一個(gè) 遮蔽屬性捏悬。 - 如果一個(gè)
foo
在[[Prototype]]
鏈的高層某處被找到撞蚕,但是它被標(biāo)記為 只讀(writable:false
) ,那么設(shè)置既存屬性和在myObject
上創(chuàng)建遮蔽屬性都是 不允許 的过牙。如果代碼運(yùn)行在strict mode
下甥厦,一個(gè)錯(cuò)誤會(huì)被拋出纺铭。否則,這個(gè)設(shè)置屬性值的操作會(huì)被無(wú)聲地忽略刀疙。不論怎樣舶赔,沒(méi)有發(fā)生遮蔽。 - 如果一個(gè)
foo
在[[Prototype]]
鏈的高層某處被找到谦秧,而且它是一個(gè)setter(見(jiàn)第三章)竟纳,那么這個(gè)setter總是被調(diào)用。沒(méi)有foo
會(huì)被添加到(也就是遮蔽在)myObject
上疚鲤,這個(gè)foo
setter也不會(huì)被重定義锥累。
大多數(shù)開(kāi)發(fā)者認(rèn)為,如果一個(gè)屬性已經(jīng)存在于[[Prototype]]
鏈的高層集歇,那么對(duì)它的賦值([[Put]]
)將總是造成遮蔽桶略。但如你所見(jiàn),這僅在剛才描述的三中場(chǎng)景中的一種(第一種)中是對(duì)的鬼悠。
如果你想在第二和第三種情況中遮蔽foo
删性,那你就不能使用=
賦值,而必須使用Object.defineProperty(..)
(見(jiàn)第三章)將foo
添加到myObject
焕窝。
注意: 第二種情況可能是三種情況中最讓人詫異的了蹬挺。只讀 屬性的存在會(huì)阻止同名屬性在[[Prototype]]
鏈的低層被創(chuàng)建(遮蔽)。這個(gè)限制的主要原因是為了增強(qiáng)類(lèi)繼承屬性的幻覺(jué)它掂。如果你想象位于鏈條高層的foo
被繼承(拷貝)至myObject
巴帮, 那么在myObject
上強(qiáng)制foo
屬性不可寫(xiě)就有道理。但如果你將幻覺(jué)和現(xiàn)實(shí)分開(kāi)虐秋,而且認(rèn)識(shí)到 實(shí)際上 沒(méi)有這樣的繼承拷貝發(fā)生(見(jiàn)第四榕茧,五章),那么僅因?yàn)槟承┢渌膶?duì)象上擁有不可寫(xiě)的foo
客给,而導(dǎo)致myObject
不能擁有foo
屬性就有些不自然用押。而且更奇怪的是,這個(gè)限制僅限于=
賦值靶剑,當(dāng)使用Object.defineProperty(..)
時(shí)不被強(qiáng)制蜻拨。
如果你需要在方法間進(jìn)行委托,方法 的遮蔽會(huì)導(dǎo)致難看的 顯式假想多態(tài)(見(jiàn)第四章)桩引。一般來(lái)說(shuō)缎讼,遮蔽與它帶來(lái)的好處相比太過(guò)復(fù)雜和微妙了,所以你應(yīng)當(dāng)盡量避免它坑匠。第六章介紹另一種設(shè)計(jì)模式血崭,它提倡干凈而且不鼓勵(lì)遮蔽。
遮蔽甚至?xí)晕⒚畹姆绞诫[含地發(fā)生,所以要想避免它必須小心夹纫⊙蚀桑考慮這段代碼:
var anotherObject = {
a: 2
};
var myObject = Object.create( anotherObject );
anotherObject.a; // 2
myObject.a; // 2
anotherObject.hasOwnProperty( "a" ); // true
myObject.hasOwnProperty( "a" ); // false
myObject.a++; // 噢,隱式遮蔽舰讹!
anotherObject.a; // 2
myObject.a; // 3
myObject.hasOwnProperty( "a" ); // true
雖然看起來(lái)myObject.a++
應(yīng)當(dāng)(通過(guò)委托)查詢(xún)并 原地 遞增anotherObject.a
屬性忱详,但是++
操作符相當(dāng)于myObject.a = myObject.a + 1
。結(jié)果就是在[[Prototype]]
上進(jìn)行a
的[[Get]]
查詢(xún)跺涤,從anotherObject.a
得到當(dāng)前的值2
匈睁,將這個(gè)值遞增1,然后將值3
用[[Put]]
賦值到myObject
上的新遮蔽屬性a
上桶错。噢航唆!
修改你的委托屬性時(shí)要非常小心。如果你想遞增anotherObject.a
院刁, 那么唯一正確的方法是anotherObject.a++
糯钙。
“類(lèi)”
現(xiàn)在你可能會(huì)想知道:“為什么 一個(gè)對(duì)象需要鏈到另一個(gè)對(duì)象?”真正的好處是什么退腥?這是一個(gè)很恰當(dāng)?shù)膯?wèn)題任岸,但在我們能夠完全理解和體味它是什么和如何有用之前,我們必須首先理解[[Prototype]]
不是 什么狡刘。
正如我們?cè)诘谒恼轮v解的享潜,在JavaScript中,對(duì)于對(duì)象來(lái)說(shuō)沒(méi)有抽象模式/藍(lán)圖嗅蔬,即沒(méi)有面向類(lèi)的語(yǔ)言中那樣的稱(chēng)為類(lèi)的東西剑按。JavaScript 只有 對(duì)象。
實(shí)際上澜术,在所有語(yǔ)言中艺蝴,JavaScript 幾乎是獨(dú)一無(wú)二的,也許是唯一的可以被稱(chēng)為“面向?qū)ο蟆钡恼Z(yǔ)言鸟废,因?yàn)榭梢愿緵](méi)有類(lèi)而直接創(chuàng)建對(duì)象的語(yǔ)言很少猜敢,而JavaScript就是其中之一。
在JavaScript中盒延,類(lèi)不能(因?yàn)楦静淮嬖冢┟枋鰧?duì)象可以做什么缩擂。對(duì)象直接定義它自己的行為。這里 僅有 對(duì)象兰英。
“類(lèi)”函數(shù)
在JavaScript中有一種奇異的行為被無(wú)恥地濫用了許多年來(lái) 山寨 成某些 看起來(lái) 像“類(lèi)”的東西撇叁。我們來(lái)仔細(xì)看看這種方式供鸠。
“某種程度的類(lèi)”這種奇特的行為取決于函數(shù)的一個(gè)奇怪的性質(zhì):所有的函數(shù)默認(rèn)都會(huì)得到一個(gè)公有的畦贸,不可枚舉的屬性,稱(chēng)為prototype
,它可以指向任意的對(duì)象薄坏。
function Foo() {
// ...
}
Foo.prototype; // { }
這個(gè)對(duì)象經(jīng)常被稱(chēng)為“Foo的原型”趋厉,因?yàn)槲覀兺ㄟ^(guò)一個(gè)不幸地被命名為Foo.prototype
的屬性引用來(lái)訪問(wèn)它。然而胶坠,我們馬上會(huì)看到君账,這個(gè)術(shù)語(yǔ)命中注定地將我們搞糊涂。為了取代它沈善,我將它稱(chēng)為“以前被認(rèn)為是Foo的原型的對(duì)象”乡数。只是開(kāi)個(gè)玩笑∥拍担“一個(gè)被隨意標(biāo)記為‘Foo點(diǎn)兒原型’的對(duì)象”净赴,怎么樣?
不管我們?cè)趺捶Q(chēng)呼它罩润,這個(gè)對(duì)象到底是什么玖翅?
解釋它的最直接的方法是,每個(gè)由調(diào)用new Foo()
(見(jiàn)第二章)而創(chuàng)建的對(duì)象將最終(有些隨意地)被[[Prototype]]
鏈接到這個(gè)“Foo點(diǎn)兒原型”對(duì)象割以。
讓我們描繪一下:
function Foo() {
// ...
}
var a = new Foo();
Object.getPrototypeOf( a ) === Foo.prototype; // true
當(dāng)通過(guò)調(diào)用new Foo()
創(chuàng)建a
時(shí)金度,會(huì)發(fā)生的事情之一(見(jiàn)第二章了解所有 四個(gè) 步驟)是,a
得到一個(gè)內(nèi)部[[Prototype]]
鏈接严沥,此鏈接鏈到Foo.prototype
所指向的對(duì)象猜极。
停一會(huì)來(lái)思考一下這句話的含義。
在面向類(lèi)的語(yǔ)言中消玄,可以制造一個(gè)類(lèi)的多個(gè) 拷貝(即“實(shí)例”)魔吐,就像從模具中沖壓出某些東西一樣。我們?cè)诘谒恼轮锌吹嚼痴遥@是因?yàn)槌跏蓟ɑ蛘呃^承)類(lèi)的處理意味著酬姆,“將行為計(jì)劃從這個(gè)類(lèi)拷貝到物理對(duì)象中”,對(duì)于每個(gè)新實(shí)例這都會(huì)發(fā)生奥溺。
但是在JavaScript中辞色,沒(méi)有這樣的拷貝處理發(fā)生。你不會(huì)創(chuàng)建類(lèi)的多個(gè)實(shí)例浮定。你可以創(chuàng)建多個(gè)對(duì)象相满,它們的[[Prototype]]
連接至一個(gè)共通對(duì)象。但默認(rèn)地桦卒,沒(méi)有拷貝發(fā)生立美,如此這些對(duì)象彼此間最終不會(huì)完全分離和切斷關(guān)系,而是 鏈接在一起方灾。
new Foo()
得到一個(gè)新對(duì)象(我們叫他a
)建蹄,這個(gè)新對(duì)象a
內(nèi)部地被[[Prototype]]
鏈接至Foo.prototype
對(duì)象碌更。
結(jié)果我們得到兩個(gè)對(duì)象,彼此鏈接洞慎。 如是而已痛单。我們沒(méi)有初始化一個(gè)對(duì)象。當(dāng)然我們也沒(méi)有做任何從一個(gè)“類(lèi)”到一個(gè)實(shí)體對(duì)象拷貝劲腿。我們只是讓兩個(gè)對(duì)象互相鏈接在一起旭绒。
事實(shí)上,這個(gè)使大多數(shù)JS開(kāi)發(fā)者無(wú)法理解的秘密焦人,是因?yàn)?code>new Foo()函數(shù)調(diào)用實(shí)際上幾乎和建立鏈接的處理沒(méi)有任何 直接 關(guān)系挥吵。它是某種偶然的副作用。new Foo()
是一個(gè)間接的花椭,迂回的方法來(lái)得到我們想要的:一個(gè)被鏈接到另一個(gè)對(duì)象的對(duì)象蔫劣。
我們能用更直接的方法得到我們想要的嗎?可以个从! 這位英雄就是Object.create(..)
脉幢。我們過(guò)會(huì)兒就談到它。
名稱(chēng)的意義何在嗦锐?
在JavaScript中嫌松,我們不從一個(gè)對(duì)象(“類(lèi)”)向另一個(gè)對(duì)象(“實(shí)例”) 拷貝。我們?cè)趯?duì)象之間制造 鏈接奕污。對(duì)于[[Prototype]]
機(jī)制萎羔,視覺(jué)上,箭頭的移動(dòng)方向是從右至左碳默,由下至上贾陷。
[圖片上傳失敗...(image-bbc00e-1515410924843)]
這種機(jī)制常被稱(chēng)為“原型繼承(prototypal inheritance)”(我們很快就用代碼說(shuō)明),它經(jīng)常被說(shuō)成是動(dòng)態(tài)語(yǔ)言版的“類(lèi)繼承”嘱根。這種說(shuō)法試圖建立在面向類(lèi)世界中對(duì)“繼承”含義的共識(shí)上髓废。但是 弄擰(意思是:抹平) 了被理解語(yǔ)義,來(lái)適應(yīng)動(dòng)態(tài)腳本该抒。
先入為主慌洪,“繼承”這個(gè)詞有很強(qiáng)烈的含義(見(jiàn)第四章)。僅僅在它前面加入“原型”來(lái)區(qū)別于JavaScript中 實(shí)際上幾乎相反 的行為凑保,使真相在泥濘般的困惑中沉睡了近二十年冈爹。
我想說(shuō),將“原型”貼在“繼承”之前很大程度上搞反了它的實(shí)際意義欧引,就像一只手拿著一個(gè)桔子频伤,另一手拿著一個(gè)蘋(píng)果,而堅(jiān)持說(shuō)蘋(píng)果是一個(gè)“紅色的桔子”芝此。無(wú)論我在它前面放什么令人困惑的標(biāo)簽憋肖,那都不會(huì)改變一個(gè)水果是蘋(píng)果而另一個(gè)是桔子的 事實(shí)因痛。
更好的方法是直白地將蘋(píng)果稱(chēng)為蘋(píng)果——使用最準(zhǔn)確和最直接的術(shù)語(yǔ)。這樣能更容易地理解它們的相似之處和 許多不同之處瞬哼,因?yàn)槲覀兌紝?duì)“蘋(píng)果”的意義有一個(gè)簡(jiǎn)單的,共享的理解租副。
由于用語(yǔ)的模糊和歧義坐慰,我相信,對(duì)于解釋JavaScript機(jī)制真正如何工作來(lái)說(shuō)用僧,“原型繼承”這個(gè)標(biāo)簽(以及試圖錯(cuò)誤地應(yīng)用所有面向類(lèi)的術(shù)語(yǔ)结胀,比如“類(lèi)”,“構(gòu)造器”责循,“實(shí)例”糟港,“多態(tài)”等)本身帶來(lái)的 危害比好處多。
“繼承”意味著 拷貝 操作院仿,而JavaScript不拷貝對(duì)象屬性(原生上秸抚,默認(rèn)地)。相反歹垫,JS在兩個(gè)對(duì)象間建立鏈接剥汤,一個(gè)對(duì)象實(shí)質(zhì)上可以將對(duì)屬性/函數(shù)的訪問(wèn) 委托 到另一個(gè)對(duì)象上。對(duì)于描述JavaScript對(duì)象鏈接機(jī)制來(lái)說(shuō)排惨,“委托”是一個(gè)準(zhǔn)確得多的術(shù)語(yǔ)吭敢。
另一個(gè)有時(shí)被扔到JavaScript旁邊的術(shù)語(yǔ)是“差分繼承”。它的想法是暮芭,我們可以用一個(gè)對(duì)象與一個(gè)更泛化的對(duì)象的 不同 來(lái)描述一個(gè)它的行為鹿驼。比如,你要解釋汽車(chē)是一種載具辕宏,與其重新描述組成一個(gè)一般載具的所有特點(diǎn)畜晰,不如只說(shuō)它有4個(gè)輪子。
如果你試著想象瑞筐,在JS中任何給定的對(duì)象都是通過(guò)委托可用的所有行為的總和舷蟀,而且 在你思維中你扁平化 所有的行為到一個(gè)有形的 東西 中,那么你就可以(八九不離十地)看到“差分繼承”是如何自圓其說(shuō)的面哼。
但正如“原型繼承”野宜,“差分繼承”假意使你的思維模型比在語(yǔ)言中物理發(fā)生的事情更重要。它忽視了這樣一個(gè)事實(shí):對(duì)象B
實(shí)際上不是一個(gè)差異結(jié)構(gòu)魔策,而是由一些定義好的特定性質(zhì)匈子,與一些沒(méi)有任何定義的“漏洞”組成的。正是通過(guò)這些“漏洞”(缺少定義)闯袒,委托可以接管并且動(dòng)態(tài)地用委托行為“填補(bǔ)”它們虎敦。
對(duì)象不是像“差分繼承”的思維模型所暗示的那樣游岳,原生默認(rèn)地,通過(guò)拷貝 扁平化到一個(gè)單獨(dú)的差異對(duì)象中其徙。如此胚迫,對(duì)于描述JavaScript的[[Prototype]]
機(jī)制如何工作來(lái)說(shuō),“差分繼承”就不是自然合理唾那。
你 可以選擇 偏向“差分繼承”這個(gè)術(shù)語(yǔ)和思維模型访锻,這是個(gè)人口味的問(wèn)題,但是不能否認(rèn)這個(gè)事實(shí):它 僅僅 符合你思維中的主觀過(guò)程闹获,不是引擎的物理行為期犬。
"構(gòu)造器"(Constructors)
讓我們回到早先的代碼:
function Foo() {
// ...
}
var a = new Foo();
到底是什么導(dǎo)致我們認(rèn)為Foo
是一個(gè)“類(lèi)”?
其一避诽,我們看到了new
關(guān)鍵字的使用龟虎,就像面向類(lèi)語(yǔ)言中人們構(gòu)建類(lèi)的對(duì)象那樣。另外沙庐,它看起來(lái)我們事實(shí)上執(zhí)行了一個(gè)類(lèi)的 構(gòu)造器 方法鲤妥,因?yàn)?code>Foo()實(shí)際上是個(gè)被調(diào)用的方法,就像當(dāng)你初始化一個(gè)真實(shí)的類(lèi)時(shí)這個(gè)類(lèi)的構(gòu)造器被調(diào)用的那樣拱雏。
為了使“構(gòu)造器”的語(yǔ)義更使人糊涂旭斥,被隨意貼上標(biāo)簽的Foo.prototype
對(duì)象還有另外一招」沤В考慮這段代碼:
function Foo() {
// ...
}
Foo.prototype.constructor === Foo; // true
var a = new Foo();
a.constructor === Foo; // true
Foo.prototype
對(duì)象默認(rèn)地(就在代碼段中第一行中聲明的地方4谷)得到一個(gè)公有的,稱(chēng)為.constructor
的不可枚舉(見(jiàn)第三章)屬性羡滑,而且這個(gè)屬性回頭指向這個(gè)對(duì)象關(guān)聯(lián)的函數(shù)(這里是Foo
)菇爪。另外,我們看到被“構(gòu)造器”調(diào)用new Foo()
創(chuàng)建的對(duì)象a
看起來(lái) 也擁有一個(gè)稱(chēng)為.constructor
的屬性柒昏,也相似地指向“創(chuàng)建它的函數(shù)”凳宙。
注意: 這實(shí)際上不是真的。a
上沒(méi)有.constructor
屬性职祷,而a.constructor
確實(shí)解析成了Foo
函數(shù)氏涩,“constructor”并不像它看起來(lái)的那樣實(shí)際意味著“被XX創(chuàng)建”。我們很快就會(huì)解釋這個(gè)奇怪的地方有梆。
哦是尖,是的,另外……根據(jù)JavaScript世界中的慣例泥耀,“類(lèi)”都以大寫(xiě)字母開(kāi)頭的單詞命名饺汹,所以使用Foo
而不是foo
強(qiáng)烈地意味著我們打算讓它成為一個(gè)“類(lèi)”。這對(duì)你來(lái)說(shuō)太明顯了痰催,對(duì)吧6荡恰迎瞧?
注意: 這個(gè)慣例是如此強(qiáng)大,以至于如果你在一個(gè)小寫(xiě)字母名稱(chēng)的方法上使用new
調(diào)用逸吵,或并沒(méi)有在一個(gè)大寫(xiě)字母開(kāi)頭的函數(shù)上使用new
凶硅,許多JS語(yǔ)法檢查器將會(huì)報(bào)告錯(cuò)誤。這是因?yàn)槲覀內(nèi)绱伺Φ叵胍贘avaScript中將(假的)“面向類(lèi)” 搞對(duì)扫皱,所以我們建立了這些語(yǔ)法規(guī)則來(lái)確保我們使用了大寫(xiě)字母足绅,即便對(duì)JS引擎來(lái)講,大寫(xiě)字母根本沒(méi)有 任何意義啸罢。
構(gòu)造器還是調(diào)用编检?
上面的代碼的段中胎食,我們?cè)噲D認(rèn)為Foo
是一個(gè)“構(gòu)造器”扰才,是因?yàn)槲覀冇?code>new調(diào)用它衙傀,而且我們觀察到它“構(gòu)建”了一個(gè)對(duì)象呢蛤。
在現(xiàn)實(shí)中获询,Foo
不會(huì)比你的程序中的其他任何函數(shù)“更像構(gòu)造器”蜓肆。函數(shù)自身 不是 構(gòu)造器杭跪。但是悄雅,當(dāng)你在普通函數(shù)調(diào)用前面放一個(gè)new
關(guān)鍵字時(shí)逢渔,這就將函數(shù)調(diào)用變成了“構(gòu)造器調(diào)用”孙蒙。事實(shí)上递雀,new
在某種意義上劫持了普通函數(shù)并將它以另一種方式調(diào)用:構(gòu)建一個(gè)對(duì)象柄延,外加這個(gè)函數(shù)要做的其他任何事。
舉個(gè)例子:
function NothingSpecial() {
console.log( "Don't mind me!" );
}
var a = new NothingSpecial();
// "Don't mind me!"
a; // {}
NothingSpecial
僅僅是一個(gè)普通的函數(shù)缀程,但當(dāng)用new
調(diào)用時(shí)搜吧,幾乎是一種副作用,它會(huì) 構(gòu)建 一個(gè)對(duì)象杨凑,并被我們賦值到a
滤奈。這個(gè) 調(diào)用 是一個(gè) 構(gòu)造器調(diào)用,但是NothingSpecial
本身并不是一個(gè) 構(gòu)造器撩满。
換句話說(shuō)蜒程,在JavaScript中,更合適的說(shuō)法是伺帘,“構(gòu)造器”是在前面 用new
關(guān)鍵字調(diào)用的任何函數(shù)昭躺。
函數(shù)不是構(gòu)造器,但是當(dāng)且僅當(dāng)new
被使用時(shí)伪嫁,函數(shù)調(diào)用是一個(gè)“構(gòu)造器調(diào)用”窍仰。
機(jī)制
僅僅是這些原因使得JavaScript中關(guān)于“類(lèi)”的討論變得命運(yùn)多舛嗎?
不全是礼殊。 JS開(kāi)發(fā)者們努力地盡可能的模擬面向類(lèi):
function Foo(name) {
this.name = name;
}
Foo.prototype.myName = function() {
return this.name;
};
var a = new Foo( "a" );
var b = new Foo( "b" );
a.myName(); // "a"
b.myName(); // "b"
這段代碼展示了另外兩種“面向類(lèi)”的花招:
this.name = name
:在每個(gè)對(duì)象(分別在a
和b
上驹吮;參照第二章關(guān)于this
綁定的內(nèi)容)上添加了.name
屬性针史,和類(lèi)的實(shí)例包裝數(shù)據(jù)值很相似。Foo.prototype.myName = ...
:這也許是更有趣的技術(shù)碟狞,它在Foo.prototype
對(duì)象上添加了一個(gè)屬性(函數(shù))∽恼恚現(xiàn)在,也許讓人驚奇族沃,a.myName()
可以工作频祝。但是是如何工作的?
在上面的代碼段中脆淹,有很強(qiáng)的傾向認(rèn)為當(dāng)a
和b
被創(chuàng)建時(shí)常空,Foo.prototype
上的屬性/函數(shù)被 拷貝 到了a
與b
倆個(gè)對(duì)象上。但是盖溺,這沒(méi)有發(fā)生漓糙。
在本章開(kāi)頭,我們解釋了[[Prototype]]
鏈烘嘱,和它作為默認(rèn)的[[Get]]
算法的一部分,如何在不能直接在對(duì)象上找到屬性引用時(shí)提供后備的查詢(xún)步驟昆禽。
于是,得益于他們被創(chuàng)建的方式蝇庭,a
和b
都最終擁有一個(gè)內(nèi)部的[[Prototype]]
鏈接鏈到Foo.prototype
醉鳖。當(dāng)無(wú)法分別在a
和b
中找到myName
時(shí),就會(huì)在Foo.prototype
上找到(通過(guò)委托哮内,見(jiàn)第六章)盗棵。
復(fù)活"構(gòu)造器"
回想我們剛才對(duì).constructor
屬性的討論,怎么看起來(lái)a.constructor === Foo
為true意味著a
上實(shí)際擁有一個(gè).constructor
屬性北发,指向Foo
纹因?不對(duì)。
這只是一種不幸的混淆鲫竞。實(shí)際上辐怕,.constructor
引用也 委托 到了Foo.prototype
,它 恰好 有一個(gè)指向Foo
的默認(rèn)屬性从绘。
這 看起來(lái) 方便得可怕寄疏,一個(gè)被Foo
構(gòu)建的對(duì)象可以訪問(wèn)指向Foo
的.constructor
屬性。但這只不過(guò)是安全感上的錯(cuò)覺(jué)僵井。它是一個(gè)歡樂(lè)的巧合陕截,幾乎是誤打誤撞,通過(guò)默認(rèn)的[[Prototype]]
委托a.constructor
恰好 指向Foo
批什。實(shí)際上.construcor
意味著“被XX構(gòu)建”這種注定失敗的臆測(cè)會(huì)以幾種方式來(lái)咬到你农曲。
第一,在Foo.prototype
上的.constructor
屬性?xún)H當(dāng)Foo
函數(shù)被聲明時(shí)才出現(xiàn)在對(duì)象上。如果你創(chuàng)建一個(gè)新對(duì)象乳规,并用它替換函數(shù)默認(rèn)的.prototype
對(duì)象引用形葬,這個(gè)新對(duì)象上將不會(huì)魔法般地得到.contructor
。
考慮這段代碼:
function Foo() { /* .. */ }
Foo.prototype = { /* .. */ }; // 創(chuàng)建一個(gè)新的prototype對(duì)象
var a1 = new Foo();
a1.constructor === Foo; // false!
a1.constructor === Object; // true!
Object(..)
沒(méi)有“構(gòu)建”a1
暮的,是吧笙以?看起來(lái)確實(shí)是Foo()
“構(gòu)建了”它。許多開(kāi)發(fā)者認(rèn)為Foo()
在執(zhí)行構(gòu)建冻辩,但當(dāng)你認(rèn)為“構(gòu)造器”意味著“被XX構(gòu)建”時(shí)猖腕,一切就都崩塌了,因?yàn)槿绻菢拥脑挘?code>a1.construcor應(yīng)當(dāng)是Foo
恨闪,但它不是倘感!
發(fā)生了什么?a1
沒(méi)有.constructor
屬性咙咽,所以它沿者[[Prototype]]
鏈向上委托到了Foo.prototype
老玛。但是這個(gè)對(duì)象也沒(méi)有.constructor
(默認(rèn)的Foo.prototype
對(duì)象就會(huì)有!)犁珠,所以它繼續(xù)委托逻炊,這次輪到了Object.prototype
互亮,委托鏈的最頂端犁享。那個(gè) 對(duì)象上確實(shí)擁有.constructor
,它指向內(nèi)建的Object(..)
函數(shù)豹休。
誤解炊昆,消除。
當(dāng)然威根,你可以把.constructor
加回到Foo.prototype
對(duì)象上凤巨,但是要做一些手動(dòng)工作,特別是如果你想要它與原生的行為吻合洛搀,并不可枚舉時(shí)(見(jiàn)第三章)敢茁。
舉例來(lái)說(shuō):
function Foo() { /* .. */ }
Foo.prototype = { /* .. */ }; // 創(chuàng)建一個(gè)新的prototype對(duì)象
// 需要正確地“修復(fù)”丟失的`.construcor`
// 新對(duì)象上的屬性以`Foo.prototype`的形式提供。
// `defineProperty(..)`的內(nèi)容見(jiàn)第三章留美。
Object.defineProperty( Foo.prototype, "constructor" , {
enumerable: false,
writable: true,
configurable: true,
value: Foo // 使`.constructor`指向`Foo`
} );
要修復(fù).constructor
要花不少功夫彰檬。而且,我們做的一切是為了延續(xù)“構(gòu)造器”意味著“被XX構(gòu)建”的誤解谎砾。這是一種昂貴的假象逢倍。
事實(shí)上,一個(gè)對(duì)象上的.construcor
默認(rèn)地隨意指向一個(gè)函數(shù)景图,而這個(gè)函數(shù)反過(guò)來(lái)?yè)碛幸粋€(gè)指向被這個(gè)對(duì)象稱(chēng)為.prototype
的對(duì)象较雕。“構(gòu)造器”和“原型”這兩個(gè)詞僅有松散的默認(rèn)含義挚币,可能是真的也可能不是真的亮蒋。最佳方案是提醒你自己扣典,“構(gòu)造器不是意味著被XX構(gòu)建”。
.constructor
不是一個(gè)魔法般不可變的屬性慎玖。它是不可枚舉的(見(jiàn)上面的代碼段)激捏,但是它的值是可寫(xiě)的(可以改變),而且凄吏,你可以在[[Prototype]]
鏈上的任何對(duì)象上添加或覆蓋(有意或無(wú)意地)名為constructor
的屬性远舅,用你感覺(jué)合適的任何值。
根據(jù)[[Get]]
算法如何遍歷[[Prototype]]
鏈痕钢,在任何地方找到的一個(gè).constructor
屬性引用解析的結(jié)果可能與你期望的十分不同图柏。
看到它的實(shí)際意義有多隨便了嗎?
結(jié)果任连?某些像a1.constructor
這樣隨意的對(duì)象屬性引用實(shí)際上不能被認(rèn)為是默認(rèn)的函數(shù)引用蚤吹。還有,我們馬上就會(huì)看到随抠,通過(guò)一個(gè)簡(jiǎn)單的省略裁着,a1.constructor
可以最終指向某些令人詫異,沒(méi)道理的地方拱她。
a1.constructor
是極其不可靠的二驰,在你的代碼中不應(yīng)依賴(lài)的不安全引用。一般來(lái)說(shuō)秉沼,這樣的引用應(yīng)當(dāng)盡量避免桶雀。
“(原型)繼承”
我們已經(jīng)看到了一些近似的“類(lèi)”機(jī)制駭進(jìn)JavaScript程序。但是如果我們沒(méi)有一種近似的“繼承”唬复,JavaScript的“類(lèi)”將會(huì)更空洞矗积。
實(shí)際上,我們已經(jīng)看到了一個(gè)常被稱(chēng)為“原型繼承”的機(jī)制如何工作:a
可以“繼承自”Foo.prototype
敞咧,并因此可以訪問(wèn)myName()
函數(shù)棘捣。但是我們傳統(tǒng)的想法認(rèn)為“繼承”是兩個(gè)“類(lèi)”間的關(guān)系,而非“類(lèi)”與“實(shí)例”的關(guān)系休建。
[圖片上傳失敗...(image-ef6f5-1515410924843)]
回想之前這幅圖乍恐,它不僅展示了從對(duì)象(也就是“實(shí)例”)a1
到對(duì)象Foo.prototype
的委托,而且從Bar.prototype
到Foo.prototype
丰包,這酷似類(lèi)繼承的親自概念禁熏。酷似,除了方向邑彪,箭頭表示的是委托鏈接瞧毙,而不是拷貝操作。
這里是一段典型的創(chuàng)建這樣的鏈接的“原型風(fēng)格”代碼:
function Foo(name) {
this.name = name;
}
Foo.prototype.myName = function() {
return this.name;
};
function Bar(name,label) {
Foo.call( this, name );
this.label = label;
}
// 這里,我們創(chuàng)建一個(gè)新的`Bar.prototype`鏈接鏈到`Foo.prototype`
Bar.prototype = Object.create( Foo.prototype );
// 注意宙彪!現(xiàn)在`Bar.prototype.constructor`不存在了矩动,
// 如果你有依賴(lài)這個(gè)屬性的習(xí)慣的話,可以被手動(dòng)“修復(fù)”释漆。
Bar.prototype.myLabel = function() {
return this.label;
};
var a = new Bar( "a", "obj a" );
a.myName(); // "a"
a.myLabel(); // "obj a"
注意: 要想知道為什么上面代碼中的this
指向a
悲没,參見(jiàn)第二章。
重要的部分是Bar.prototype = Object.create( Foo.prototype )
男图。Object.create(..)
憑空 創(chuàng)建 了一個(gè)“新”對(duì)象示姿,并將這個(gè)新對(duì)象內(nèi)部的[[Prototype]]
鏈接到你指定的對(duì)象上(在這里是Foo.prototype
)。
換句話說(shuō)逊笆,這一行的意思是:“做一個(gè) 新的 鏈接到‘Foo點(diǎn)兒prototype’的‘Bar點(diǎn)兒prototype’對(duì)象”栈戳。
當(dāng)function Bar() { .. }
被聲明時(shí),就像其他函數(shù)一樣难裆,擁有一個(gè)鏈到默認(rèn)對(duì)象的.prototype
鏈接子檀。但是 那個(gè) 對(duì)象沒(méi)有鏈到我們希望的Foo.prototype
。所以乃戈,我們創(chuàng)建了一個(gè) 新 對(duì)象褂痰,鏈到我們希望的地方,并將原來(lái)的錯(cuò)誤鏈接的對(duì)象扔掉症虑。
注意: 這里一個(gè)常見(jiàn)的誤解/困惑是缩歪,下面兩種方法 也 能工作,但是他們不會(huì)如你期望的那樣工作:
// 不會(huì)如你期望的那樣工作!
Bar.prototype = Foo.prototype;
// 會(huì)如你期望的那樣工作
// 但會(huì)帶有你可能不想要的副作用 :(
Bar.prototype = new Foo();
Bar.prototype = Foo.prototype
不會(huì)創(chuàng)建新對(duì)象讓Bar.prototype
鏈接侦讨。它只是讓Bar.prototype
成為Foo.prototype
的另一個(gè)引用驶冒,將Bar
直接鏈到Foo
鏈著的 同一個(gè)對(duì)象:Foo.prototype
苟翻。這意味著當(dāng)你開(kāi)始賦值時(shí)韵卤,比如Bar.prototype.myLabel = ...
,你修改的 不是一個(gè)分離的對(duì)象 而是那個(gè)被分享的Foo.prototype
對(duì)象本身崇猫,它將影響到所有鏈接到Foo.prototype
的對(duì)象沈条。這幾乎可以確定不是你想要的。如果這正是你想要的诅炉,那么你根本就不需要Bar
蜡歹,你應(yīng)當(dāng)僅使用Foo
來(lái)使你的代碼更簡(jiǎn)單。
Bar.prototype = new Foo()
確實(shí) 創(chuàng)建了一個(gè)新的對(duì)象涕烧,這個(gè)新對(duì)象也的確鏈接到了我們希望的Foo.prototype
月而。但是,它是用Foo(..)
“構(gòu)造器調(diào)用”來(lái)這樣做的议纯。如果這個(gè)函數(shù)有任何副作用(比如logging父款,改變狀態(tài),注冊(cè)其他對(duì)象,向this
添加數(shù)據(jù)屬性憨攒,等等)世杀,這些副作用就會(huì)在鏈接時(shí)發(fā)生(而且很可能是對(duì)錯(cuò)誤的對(duì)象!)肝集,而不是像可能希望的那樣瞻坝,僅最終在Bar()
的“后裔”被創(chuàng)建時(shí)發(fā)生。
于是杏瞻,我們剩下的選擇就是使用Object.create(..)
來(lái)制造一個(gè)新對(duì)象所刀,這個(gè)對(duì)象被正確地鏈接,而且沒(méi)有調(diào)用Foo(..)
時(shí)所產(chǎn)生的副作用捞挥。一個(gè)輕微的缺點(diǎn)是勉痴,我們不得不創(chuàng)建新對(duì)象,并把舊的扔掉树肃,而不是修改提供給我們的默認(rèn)既存對(duì)象蒸矛。
如果有一種標(biāo)準(zhǔn)且可靠地方法來(lái)修改既存對(duì)象的鏈接就好了。ES6之前胸嘴,有一個(gè)非標(biāo)準(zhǔn)的雏掠,而且不是完全對(duì)所有瀏覽器通用的方法:通過(guò)可以設(shè)置的.__proto__
屬性。ES6中增加了Object.setPrototypeOf(..)
輔助工具劣像,它提供了標(biāo)準(zhǔn)且可預(yù)見(jiàn)的方法乡话。
讓我們一對(duì)一地比較ES6之前和ES6標(biāo)準(zhǔn)的技術(shù)如何處理將Bar.prototype
鏈接至Foo.prototype
:
// ES6以前
// 扔掉默認(rèn)既存的`Bar.prototype`
Bar.prototype = Object.create( Foo.prototype );
// ES6+
// 修改既存的`Bar.prototype`
Object.setPrototypeOf( Bar.prototype, Foo.prototype );
如果忽略Object.create(..)
方式在性能上的輕微劣勢(shì)(扔掉一個(gè)對(duì)象,然后被回收)耳奕,其實(shí)它相對(duì)短一些而且可能比ES6+的方式更易讀绑青。但兩種方式可能都只是語(yǔ)法表面現(xiàn)象。
考察“類(lèi)”關(guān)系
如果你有一個(gè)對(duì)象a
并且希望找到它委托至哪個(gè)對(duì)象呢(如果有的話)屋群?考察一個(gè)實(shí)例(一個(gè)JS對(duì)象)的繼承血統(tǒng)(在JS中是委托鏈接)闸婴,在傳統(tǒng)的面向類(lèi)環(huán)境中稱(chēng)為 自省(introspection)(或 反射(reflection))芍躏。
考慮下面的代碼:
function Foo() {
// ...
}
Foo.prototype.blah = ...;
var a = new Foo();
那么我們?nèi)绾巫允?code>a來(lái)找到它的“祖先”(委托鏈)呢邪乍?一種方式是接受“類(lèi)”的困惑:
a instanceof Foo; // true
instanceof
操作符的左邊操作數(shù)接收一個(gè)普通對(duì)象,右邊操作數(shù)接收一個(gè) 函數(shù)对竣。instanceof
回答的問(wèn)題是:在a
的整個(gè)[[Prototype]]
鏈中庇楞,有沒(méi)有出現(xiàn)被那個(gè)被Foo.prototype
所隨便指向的對(duì)象?
不幸的是否纬,這意味著如果你擁有可以用于測(cè)試的 函數(shù)(Foo
吕晌,和它帶有的.prototype
引用),你只能查詢(xún)某些對(duì)象(a
)的“祖先”临燃。如果你有兩個(gè)任意的對(duì)象睛驳,比如a
和b
壁拉,而且你想調(diào)查是否 這些對(duì)象 通過(guò)[[Prototype]]
鏈相互關(guān)聯(lián),單靠instanceof
幫不上什么忙柏靶。
注意: 如果你使用內(nèi)建的.bind(..)
工具來(lái)制造一個(gè)硬綁定的函數(shù)(見(jiàn)第二章)弃理,這個(gè)被創(chuàng)建的函數(shù)將不會(huì)擁有.prototype
屬性。將instanceof
與這樣的函數(shù)一起使用時(shí)屎蜓,將會(huì)透明地替換為創(chuàng)建這個(gè)硬綁定函數(shù)的 目標(biāo)函數(shù) 的.prototype
痘昌。
將硬綁定函數(shù)用于“構(gòu)造器調(diào)用”十分罕見(jiàn),但如果你這么做炬转,它會(huì)表現(xiàn)得好像是 目標(biāo)函數(shù) 被調(diào)用了辆苔,這意味著將instanceof
與硬綁定函數(shù)一起使用也會(huì)參照原版函數(shù)。
下面這段代碼展示了試圖通過(guò)“類(lèi)”的語(yǔ)義和instanceof
來(lái)推導(dǎo) 兩個(gè)對(duì)象 間的關(guān)系是多么荒謬:
// 用來(lái)檢查`o1`是否關(guān)聯(lián)到(委托至)`o2`的幫助函數(shù)
function isRelatedTo(o1, o2) {
function F(){}
F.prototype = o2;
return o1 instanceof F;
}
var a = {};
var b = Object.create( a );
isRelatedTo( b, a ); // true
在isRelatedTo(..)
內(nèi)部扼劈,我們借用一個(gè)一次性的函數(shù)F
驻啤,重新對(duì)它的.prototype
賦值,使他隨意地指向某個(gè)對(duì)象o2
荐吵,之后問(wèn)是否o1
是F
的“一個(gè)實(shí)例”骑冗。很明顯,o1
實(shí)際上不是繼承或遺傳自F
先煎,甚至不是由F
構(gòu)建的贼涩,所以顯而易見(jiàn)這種實(shí)踐是愚蠢且讓人困惑的。這個(gè)問(wèn)題歸根結(jié)底是將類(lèi)的語(yǔ)義強(qiáng)加于JavaScript的尷尬薯蝎,在這個(gè)例子中是由instanceof
的間接語(yǔ)義揭露的遥倦。
第二種,也是更干凈的方式占锯,[[Prototype]]
反射:
Foo.prototype.isPrototypeOf( a ); // true
注意在這種情況下袒哥,我們并不真正關(guān)心(甚至 不需要)Foo
,我們僅需要一個(gè) 對(duì)象(在我們的例子中就是隨意標(biāo)志為Foo.prototype
)來(lái)與另一個(gè) 對(duì)象 測(cè)試消略。isPrototypeOf(..)
回答的問(wèn)題是:在a
的整個(gè)[[Prototype]]
鏈中堡称,Foo.prototype
出現(xiàn)過(guò)嗎?
同樣的問(wèn)題疑俭,和完全同樣的答案粮呢。但是在第二種方式中,我們實(shí)際上不需要間接地引用一個(gè).prototype
屬性將被自動(dòng)查詢(xún)的 函數(shù)(Foo
)钞艇。
我們 只需要 兩個(gè) 對(duì)象 來(lái)考察它們之間的關(guān)系。比如:
// 簡(jiǎn)單地:`b`在`c`的`[[Prototype]]`鏈中出現(xiàn)過(guò)嗎豪硅?
b.isPrototypeOf( c );
注意哩照,這種方法根本不要求有一個(gè)函數(shù)(“類(lèi)”)。它僅僅使用對(duì)象的直接引用b
和c
懒浮,來(lái)查詢(xún)他們的關(guān)系飘弧。換句話說(shuō)识藤,我們上面的isRelatedTo(..)
工具是內(nèi)建在語(yǔ)言中的,它的名字叫isPrototypeOf(..)
次伶。
我們也可以直接取得一個(gè)對(duì)象的[[Prototype]]
痴昧。在ES5中,這么做的標(biāo)準(zhǔn)方法是:
Object.getPrototypeOf( a );
而且你將注意到對(duì)象引用是我們期望的:
Object.getPrototypeOf( a ) === Foo.prototype; // true
大多數(shù)瀏覽器(不是全部9谕酢)還一種長(zhǎng)期支持的赶撰,非標(biāo)準(zhǔn)方法可以訪問(wèn)內(nèi)部的[[Prototype]]
:
a.__proto__ === Foo.prototype; // true
這個(gè)奇怪的.__proto__
(直到ES6才標(biāo)準(zhǔn)化!)屬性“魔法般地”取得一個(gè)對(duì)象內(nèi)部的[[Prototype]]
作為引用柱彻,如果你想要直接考察(甚至遍歷:.__proto__.__proto__...
)[[Prototype]]
鏈豪娜,這個(gè)引用十分有用。
和我們?cè)缦瓤吹降?code>.constructor一樣哟楷,.__proto__
實(shí)際上不存在于你考察的對(duì)象上(在我們的例子中是a
)瘤载。事實(shí)上,它存在于(不可枚舉地卖擅;見(jiàn)第二章)內(nèi)建的Object.prototype
上鸣奔,和其他的共通工具在一起(.toString()
, .isPrototypeOf(..)
, 等等)。
而且惩阶,.__proto__
看起來(lái)像一個(gè)屬性溃蔫,但實(shí)際上將它看做是一個(gè)getter/setter(見(jiàn)第三章)更合適。
大致地琳猫,我們可以這樣描述.__proto__
實(shí)現(xiàn)(見(jiàn)第三章伟叛,對(duì)象屬性的定義):
Object.defineProperty( Object.prototype, "__proto__", {
get: function() {
return Object.getPrototypeOf( this );
},
set: function(o) {
// setPrototypeOf(..) as of ES6
Object.setPrototypeOf( this, o );
return o;
}
} );
所以,當(dāng)我們?cè)L問(wèn)a.__proto__
(取得它的值)時(shí)脐嫂,就好像調(diào)用a.__proto__()
(調(diào)用getter函數(shù))统刮。雖然getter函數(shù)存在于Object.prototype
上(參照第二章,this
綁定規(guī)則)账千,但這個(gè)函數(shù)調(diào)用將a
用作它的this
侥蒙,所以它相當(dāng)于在說(shuō)Object.getPrototypeOf( a )
。
.__proto__
還是一個(gè)可設(shè)置的屬性匀奏,就像早先展示過(guò)的ES6Object.setPrototypeOf(..)
鞭衩。然而,一般來(lái)說(shuō)你 不應(yīng)該改變一個(gè)既存對(duì)象的[[Prototype]]
娃善。
在某些允許對(duì)Array
定義“子類(lèi)”的框架中论衍,深度地使用了一些非常復(fù)雜,高級(jí)的技術(shù)聚磺,但是在一般的編程實(shí)踐中經(jīng)常是讓人皺眉頭的坯台,因?yàn)檫@通常導(dǎo)致非常難理解/維護(hù)的代碼。
注意: 在ES6中瘫寝,關(guān)鍵字class
將允許某些近似方法蜒蕾,對(duì)像Array
這樣的內(nèi)建類(lèi)型“定義子類(lèi)”稠炬。參見(jiàn)附錄A中關(guān)于ES6中加入的class
的討論。
僅有一小部分例外(就像前面提到過(guò)的)咪啡,會(huì)設(shè)置一個(gè)默認(rèn)函數(shù).prototype
對(duì)象的[[Prototype]]
首启,使它引用其他的對(duì)象(Object.prototype
之外的對(duì)象)。它們會(huì)避免將這個(gè)默認(rèn)對(duì)象完全替換為一個(gè)新的鏈接對(duì)象撤摸。否則毅桃,為了在以后更容易地閱讀你的代碼 最好將對(duì)象的[[Prototype]]
鏈接作為只讀性質(zhì)對(duì)待。
注意: 針對(duì)雙下劃線愁溜,特別是在像__proto__
這樣的屬性中開(kāi)頭的部分疾嗅,JavaScript社區(qū)非官方地創(chuàng)造了一個(gè)術(shù)語(yǔ):“dunder”。所以冕象,那些JavaScript的“酷小子”們通常將__proto__
讀作“dunder proto”代承。
對(duì)象鏈接
正如我們看到的,[[Prototype]]
機(jī)制是一個(gè)內(nèi)部鏈接渐扮,它存在于一個(gè)對(duì)象上论悴,這個(gè)對(duì)象引用一些其他的對(duì)象。
這種鏈接(主要)在對(duì)第一個(gè)對(duì)象進(jìn)行屬性/方法引用墓律,但這樣的屬性/方法不存在時(shí)實(shí)施膀估。在這種情況下,[[Prototype]]
鏈接告訴引擎在那個(gè)被鏈接的對(duì)象上查找這個(gè)屬性/方法耻讽。接下來(lái)察纯,如果這個(gè)對(duì)象不能滿(mǎn)足查詢(xún),它的[[Prototype]]
又會(huì)被查找针肥,如此繼續(xù)饼记。這個(gè)在對(duì)象間的一系列鏈接構(gòu)成了所謂的“原形鏈”。
創(chuàng)建鏈接
我們已經(jīng)徹底揭露了為什么JavaScript的[[Prototype]]
機(jī)制和 類(lèi) 不 一樣慰枕,而且我們也看到了如何在正確的對(duì)象間創(chuàng)建 鏈接具则。
[[Prototype]]
機(jī)制的意義是什么?為什么總是見(jiàn)到JS開(kāi)發(fā)者們費(fèi)那么大力氣(模擬類(lèi))在他們的代碼中搞亂這些鏈接具帮?
記得我們?cè)诒菊潞芸壳暗牡胤秸f(shuō)過(guò)Object.create(..)
是英雄嗎博肋?現(xiàn)在,我們準(zhǔn)備好看看為什么了蜂厅。
var foo = {
something: function() {
console.log( "Tell me something good..." );
}
};
var bar = Object.create( foo );
bar.something(); // Tell me something good...
Object.create(..)
創(chuàng)建了一個(gè)鏈接到我們指定的對(duì)象(foo
)上的新對(duì)象(bar
)匪凡,這給了我們[[Prototype]]
機(jī)制的所有力量(委托),而且沒(méi)有new
函數(shù)作為類(lèi)和構(gòu)造器調(diào)用產(chǎn)生的任何沒(méi)必要的復(fù)雜性葛峻,搞亂.prototype
和.constructor
引用锹雏,或任何其他的多余的東西。
注意: Object.create(null)
創(chuàng)建一個(gè)擁有空(也就是null
)[[Prototype]]
鏈接的對(duì)象术奖,如此這個(gè)對(duì)象不能委托到任何地方礁遵。因?yàn)檫@樣的對(duì)象沒(méi)有原形鏈,instancof
操作符(前面解釋過(guò))沒(méi)有東西可檢查采记,所以它總返回false
佣耐。由于他們典型的用途是在屬性中存儲(chǔ)數(shù)據(jù),這種特殊的空[[Prototype]]
對(duì)象經(jīng)常被稱(chēng)為“dictionaries(字典)”唧龄,這主要是因?yàn)樗鼈儧](méi)有可能受到在[[Prototype]]
鏈上任何委托屬性/函數(shù)的影響兼砖,所以它們是純粹的扁平數(shù)據(jù)存儲(chǔ)。
我們不 需要 類(lèi)來(lái)在兩個(gè)對(duì)象間創(chuàng)建有意義的關(guān)系既棺。我們需要 真正關(guān)心 的唯一問(wèn)題是對(duì)象為了委托而鏈接在一起讽挟,而Object.create(..)
給我們這種鏈接并且沒(méi)有一切關(guān)于類(lèi)的爛設(shè)計(jì)。
填補(bǔ)Object.create()
Object.create(..)
在ES5中被加入丸冕。你可能需要支持ES5之前的環(huán)境(比如老版本的IE)耽梅,所以讓我們來(lái)看一個(gè)Object.create(..)
的簡(jiǎn)單 部分 填補(bǔ)工具,它甚至能在更老的JS環(huán)境中給我們所需的能力:
if (!Object.create) {
Object.create = function(o) {
function F(){}
F.prototype = o;
return new F();
};
}
這個(gè)填補(bǔ)工具通過(guò)一個(gè)一次性的F
函數(shù)并覆蓋它的.prototype
屬性來(lái)指向我們想連接到的對(duì)象胖烛。之后我們用new F()
構(gòu)造器調(diào)用來(lái)制造一個(gè)將會(huì)鏈到我們指定對(duì)象上的新對(duì)象眼姐。
Object.create(..)
的這種用法是目前最常見(jiàn)的用法,因?yàn)樗倪@一部分是 可以 填補(bǔ)的佩番。ES5標(biāo)準(zhǔn)的內(nèi)建Object.create(..)
還提供了一個(gè)附加的功能众旗,它是 不能 被ES5之前的版本填補(bǔ)的。如此趟畏,這個(gè)功能的使用遠(yuǎn)沒(méi)有那么常見(jiàn)贡歧。為了完整性,讓我么看看這個(gè)附加功能:
var anotherObject = {
a: 2
};
var myObject = Object.create( anotherObject, {
b: {
enumerable: false,
writable: true,
configurable: false,
value: 3
},
c: {
enumerable: true,
writable: false,
configurable: false,
value: 4
}
} );
myObject.hasOwnProperty( "a" ); // false
myObject.hasOwnProperty( "b" ); // true
myObject.hasOwnProperty( "c" ); // true
myObject.a; // 2
myObject.b; // 3
myObject.c; // 4
Object.create(..)
的第二個(gè)參數(shù)指定了要添加在新對(duì)象上的屬性名赋秀,通過(guò)聲明每個(gè)新屬性的 屬性描述符(見(jiàn)第三章)利朵。因?yàn)樵贓S5之前的環(huán)境中填補(bǔ)屬性描述符是不可能的,所以Object.create(..)
的這個(gè)附加功能無(wú)法填補(bǔ)沃琅。
因?yàn)?code>Object.create(..)的絕大多數(shù)用途都是使用填補(bǔ)安全的功能子集哗咆,所以大多數(shù)開(kāi)發(fā)者在ES5之前的環(huán)境中使用這種 部分填補(bǔ) 也沒(méi)有問(wèn)題。
有些開(kāi)發(fā)者采取嚴(yán)格得多的觀點(diǎn)益眉,也就是除非能夠被 完全 填補(bǔ)晌柬,否則沒(méi)有函數(shù)應(yīng)該被填補(bǔ)。因?yàn)?code>Object.create(..)可以部分填補(bǔ)的工具之一郭脂,這種較狹窄的觀點(diǎn)會(huì)說(shuō)年碘,如果你需要在ES5之前的環(huán)境中使用Object.create(..)
的任何功能,你應(yīng)當(dāng)使用自定義的工具展鸡,而不是填充屿衅,而且應(yīng)當(dāng)徹底遠(yuǎn)離使用Object.create
這個(gè)名字。你可以定義自己的工具莹弊,比如:
function createAndLinkObject(o) {
function F(){}
F.prototype = o;
return new F();
}
var anotherObject = {
a: 2
};
var myObject = createAndLinkObject( anotherObject );
myObject.a; // 2
我不會(huì)分享這種嚴(yán)格的觀點(diǎn)涤久。我完全擁護(hù)如上面展示的Object.create(..)
的常見(jiàn)部分填補(bǔ)涡尘,甚至在ES5之前的環(huán)境下在你的代碼中使用它。我將選擇權(quán)留給你响迂。
鏈接作為候補(bǔ)考抄?
也許這么想很吸引人:這些對(duì)象間的鏈接 主要 是為了給“缺失”的屬性和方法提供某種候補(bǔ)。雖然這是一個(gè)可觀察到的結(jié)果蔗彤,但是我不認(rèn)為這是考慮[[Prototype]]
的正確方法川梅。
考慮下面的代碼:
var anotherObject = {
cool: function() {
console.log( "cool!" );
}
};
var myObject = Object.create( anotherObject );
myObject.cool(); // "cool!"
得益于[[Prototype]]
,這段代碼可以工作然遏,但如果你這樣寫(xiě)是為了 萬(wàn)一 myObject
不能處理某些開(kāi)發(fā)者可能會(huì)調(diào)用的屬性/方法贫途,而讓anotherObject
作為一個(gè)候補(bǔ),你的軟件大概會(huì)變得有點(diǎn)兒“魔法”并且更難于理解和維護(hù)待侵。
這不是說(shuō)候補(bǔ)在任何情況下都不是一個(gè)合適的設(shè)計(jì)模式丢早,但它不是一個(gè)在JS中很常見(jiàn)的用法,所以如果你發(fā)現(xiàn)自己在這么做诫给,那么你可能想要退一步并重新考慮它是否真的是合適且合理的設(shè)計(jì)香拉。
注意: 在ES6中,引入了一個(gè)稱(chēng)為Proxy(代理)
的高級(jí)功能中狂,它可以提供某種“方法未找到”類(lèi)型的行為凫碌。Proxy
超出了本書(shū)的范圍,但會(huì)在以后的 “你不懂JS” 系列圖書(shū)中詳細(xì)講解胃榕。
這里不要錯(cuò)過(guò)一個(gè)重要的細(xì)節(jié)盛险。
例如,你打算為一個(gè)開(kāi)發(fā)者設(shè)計(jì)軟件勋又,如果即使在myObject
上沒(méi)有cool()
方法時(shí)調(diào)用myObject.cool()
也能工作苦掘,會(huì)在你的API設(shè)計(jì)上引入一些“魔法”氣息,這可能會(huì)使未來(lái)維護(hù)你的軟件的開(kāi)發(fā)者很吃驚楔壤。
然而你可以在你的API設(shè)計(jì)上少用些“魔法”鹤啡,而仍然利用[[Prototype]]
鏈接的力量。
var anotherObject = {
cool: function() {
console.log( "cool!" );
}
};
var myObject = Object.create( anotherObject );
myObject.doCool = function() {
this.cool(); // internal delegation!
};
myObject.doCool(); // "cool!"
這里蹲嚣,我們調(diào)用myObject.doCool()
递瑰,它是一個(gè) 實(shí)際存在于 myObject
上的方法,這使我們的API設(shè)計(jì)更清晰(沒(méi)那么“魔法”)隙畜。在它內(nèi)部抖部,我們的實(shí)現(xiàn)依照 委托設(shè)計(jì)模式(見(jiàn)第六章),利用[[Prototype]]
委托到anotherObject.cool()
议惰。
換句話說(shuō)慎颗,如果委托是一個(gè)內(nèi)部實(shí)現(xiàn)細(xì)節(jié),而非在你的API結(jié)構(gòu)設(shè)計(jì)中簡(jiǎn)單地暴露出來(lái),它傾向于減少意外/困惑俯萎。我們會(huì)在下一章中詳細(xì)解釋 委托傲宜。
復(fù)習(xí)
當(dāng)試圖在一個(gè)對(duì)象上進(jìn)行屬性訪問(wèn),而對(duì)象沒(méi)有該屬性時(shí)讯屈,對(duì)象內(nèi)部的[[Prototype]]
鏈接定義了[[Get]]
操作(見(jiàn)第三章)下一步應(yīng)當(dāng)?shù)侥睦飳ふ宜翱蕖_@種對(duì)象到對(duì)象的串行鏈接定義了對(duì)象的“原形鏈”(和嵌套的作用域鏈有些相似)县习,在解析屬性時(shí)發(fā)揮作用涮母。
所有普通的對(duì)象用內(nèi)建的Object.prototype
作為原形鏈的頂端(就像作用域查詢(xún)的頂端是全局作用域),如果屬性沒(méi)能在鏈條的前面任何地方找到躁愿,屬性解析就會(huì)在這里停止叛本。toString()
,valueOf()
彤钟,和其他幾種共同工具都存在于這個(gè)Object.prototype
對(duì)象上来候,這解釋了語(yǔ)言中所有的對(duì)象是如何能夠訪問(wèn)他們的。
使兩個(gè)對(duì)象相互鏈接在一起的最常見(jiàn)的方法是將new
關(guān)鍵字與函數(shù)調(diào)用一起使用逸雹,在它的四個(gè)步驟中(見(jiàn)第二章)营搅,就會(huì)建立一個(gè)新對(duì)象鏈接到另一個(gè)對(duì)象。
那個(gè)用new
調(diào)用的函數(shù)有一個(gè)被隨便地命名為.prototype
的屬性梆砸,這個(gè)屬性所引用的對(duì)象恰好就是這個(gè)新對(duì)象鏈接到的“另一個(gè)對(duì)象”转质。帶有new
的函數(shù)調(diào)用通常被稱(chēng)為“構(gòu)造器”,盡管實(shí)際上它們并沒(méi)有像傳統(tǒng)的面相類(lèi)語(yǔ)言那樣初始化一個(gè)類(lèi)帖世。
雖然這些JavaScript機(jī)制看起來(lái)和傳統(tǒng)面向類(lèi)語(yǔ)言的“初始化類(lèi)”和“類(lèi)繼承”類(lèi)似休蟹,而在JavaScript中的關(guān)鍵區(qū)別是,沒(méi)有拷貝發(fā)生日矫。取而代之的是對(duì)象最終通過(guò)[[Prototype]]
鏈鏈接在一起赂弓。
由于各種原因,不光是前面提到的術(shù)語(yǔ)哪轿,“繼承”(和“原型繼承”)與所有其他的OO用語(yǔ)盈魁,在考慮JavaScript實(shí)際如何工作時(shí)都沒(méi)有道理捶箱。
相反偎捎,“委托”是一個(gè)更確切的術(shù)語(yǔ),因?yàn)檫@些關(guān)系不是 拷貝 而是委托 鏈接堡妒。