你不懂JS:this與對(duì)象原型 第五章:原型(Prototype)

官方中文版原文鏈接

感謝社區(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]]鏈的更高層:

  1. 如果一個(gè)普通的名為foo的數(shù)據(jù)訪問(wèn)屬性在[[Prototype]]鏈的高層某處被找到眼虱,而且沒(méi)有被標(biāo)記為只讀(writable:false喻奥,那么一個(gè)名為foo的新屬性就直接添加到myObject上,形成一個(gè) 遮蔽屬性捏悬。
  2. 如果一個(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ā)生遮蔽
  3. 如果一個(gè)foo[[Prototype]]鏈的高層某處被找到谦秧,而且它是一個(gè)setter(見(jiàn)第三章)竟纳,那么這個(gè)setter總是被調(diào)用。沒(méi)有foo會(huì)被添加到(也就是遮蔽在)myObject上疚鲤,這個(gè)foosetter也不會(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)”的花招:

  1. this.name = name:在每個(gè)對(duì)象(分別在ab上驹吮;參照第二章關(guān)于this綁定的內(nèi)容)上添加了.name屬性针史,和類(lèi)的實(shí)例包裝數(shù)據(jù)值很相似。

  2. Foo.prototype.myName = ...:這也許是更有趣的技術(shù)碟狞,它在Foo.prototype對(duì)象上添加了一個(gè)屬性(函數(shù))∽恼恚現(xiàn)在,也許讓人驚奇族沃,a.myName()可以工作频祝。但是是如何工作的?

在上面的代碼段中脆淹,有很強(qiáng)的傾向認(rèn)為當(dāng)ab被創(chuàng)建時(shí)常空,Foo.prototype上的屬性/函數(shù)被 拷貝 到了ab倆個(gè)對(duì)象上。但是盖溺,這沒(méi)有發(fā)生漓糙。

在本章開(kāi)頭,我們解釋了[[Prototype]]鏈烘嘱,和它作為默認(rèn)的[[Get]]算法的一部分,如何在不能直接在對(duì)象上找到屬性引用時(shí)提供后備的查詢(xún)步驟昆禽。

于是,得益于他們被創(chuàng)建的方式蝇庭,ab都最終擁有一個(gè)內(nèi)部的[[Prototype]]鏈接鏈到Foo.prototype醉鳖。當(dāng)無(wú)法分別在ab中找到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.prototypeFoo.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ì)象睛驳,比如ab壁拉,而且你想調(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)是否o1F的“一個(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ì)象的直接引用bc懒浮,來(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)系不是 拷貝 而是委托 鏈接堡妒。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末褐奴,一起剝皮案震驚了整個(gè)濱河市按脚,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌敦冬,老刑警劉巖辅搬,帶你破解...
    沈念sama閱讀 206,126評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡堪遂,警方通過(guò)查閱死者的電腦和手機(jī)介蛉,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)溶褪,“玉大人币旧,你說(shuō)我怎么就攤上這事≡陈瑁” “怎么了吹菱?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,445評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)彭则。 經(jīng)常有香客問(wèn)我鳍刷,道長(zhǎng),這世上最難降的妖魔是什么俯抖? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,185評(píng)論 1 278
  • 正文 為了忘掉前任输瓜,我火速辦了婚禮,結(jié)果婚禮上芬萍,老公的妹妹穿的比我還像新娘尤揣。我一直安慰自己,他們只是感情好柬祠,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布北戏。 她就那樣靜靜地躺著,像睡著了一般瓶盛。 火紅的嫁衣襯著肌膚如雪最欠。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 48,970評(píng)論 1 284
  • 那天惩猫,我揣著相機(jī)與錄音芝硬,去河邊找鬼。 笑死轧房,一個(gè)胖子當(dāng)著我的面吹牛拌阴,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播奶镶,決...
    沈念sama閱讀 38,276評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼迟赃,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了厂镇?” 一聲冷哼從身側(cè)響起纤壁,我...
    開(kāi)封第一講書(shū)人閱讀 36,927評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎捺信,沒(méi)想到半個(gè)月后酌媒,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,400評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評(píng)論 2 323
  • 正文 我和宋清朗相戀三年秒咨,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了喇辽。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 37,997評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡雨席,死狀恐怖菩咨,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情陡厘,我是刑警寧澤抽米,帶...
    沈念sama閱讀 33,646評(píng)論 4 322
  • 正文 年R本政府宣布,位于F島的核電站雏亚,受9級(jí)特大地震影響缨硝,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜罢低,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望胖笛。 院中可真熱鬧网持,春花似錦、人聲如沸长踊。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,204評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)身弊。三九已至辟汰,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間阱佛,已是汗流浹背帖汞。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,423評(píng)論 1 260
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留凑术,地道東北人翩蘸。 一個(gè)月前我還...
    沈念sama閱讀 45,423評(píng)論 2 352
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像淮逊,于是被迫代替她去往敵國(guó)和親催首。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容