感謝社區(qū)中各位的大力支持,譯者再次奉上一點(diǎn)點(diǎn)福利:阿里云產(chǎn)品券碌尔,享受所有官網(wǎng)優(yōu)惠苍匆,并抽取幸運(yùn)大獎(jiǎng):點(diǎn)擊這里領(lǐng)取
接著我們上一章對(duì)對(duì)象的探索,我們很自然的將注意力轉(zhuǎn)移到“面向?qū)ο螅∣O)編程”函荣,與“類(class)”显押。我們先將“面向類”作為設(shè)計(jì)模式來看看扳肛,之后我們?cè)倏疾臁邦悺钡臋C(jī)制:“實(shí)例化(instantiation)”, “繼承(inheritance)”與“相對(duì)多態(tài)(relative polymorphism)”。
我們將會(huì)看到煮落,這些概念并不是非常自然地映射到JS的對(duì)象機(jī)制上敞峭,以及許多JavaScript開發(fā)者為了克服這些挑戰(zhàn)所做的努力(mixins等)。
注意: 這一章花了相當(dāng)一部分時(shí)間(前一半2醭稹)在著重解釋“面向?qū)ο缶幊獭崩碚撋闲铩T诤蟀氩糠钟懻摗癕ixins(混合)”時(shí),我們最終會(huì)將這些理論與真實(shí)且實(shí)際的JavaScript代碼聯(lián)系起來轿衔。但是這里首先要蹚過許多概念和假想代碼沉迹,所以可別迷路了——堅(jiān)持下去!
類(Class)理論
“類/繼承”描述了一種特定的代碼組織和結(jié)構(gòu)形式——一種在我們的軟件中對(duì)真實(shí)世界的建模方法害驹。
OO或者面向類的編程強(qiáng)調(diào)數(shù)據(jù)和操作它的行為之間有固有的聯(lián)系(當(dāng)然鞭呕,依數(shù)據(jù)的類型和性質(zhì)不同而不同!)宛官,所以合理的設(shè)計(jì)是將數(shù)據(jù)和行為打包在一起(也稱為封裝)葫松。這有時(shí)在正式的計(jì)算機(jī)科學(xué)中稱為“數(shù)據(jù)結(jié)構(gòu)”。
比如底洗,表示一個(gè)單詞或短語(yǔ)的一系列字符通常稱為“string(字符串)”腋么。這些字符就是數(shù)據(jù)。但你幾乎從來不關(guān)心數(shù)據(jù)亥揖,你總是想對(duì)數(shù)據(jù) 做事情珊擂, 所以可以 向 數(shù)據(jù)實(shí)施的行為(計(jì)算它的長(zhǎng)度,在末尾添加數(shù)據(jù)费变,檢索摧扇,等等)都被設(shè)計(jì)成為String
類的方法。
任何給定的字符串都是這個(gè)類的一個(gè)實(shí)例挚歧,這個(gè)類是一個(gè)整齊的集合包裝:字符數(shù)據(jù)和我們可以對(duì)它進(jìn)行的操作功能扛稽。
類還隱含著對(duì)一個(gè)特定數(shù)據(jù)結(jié)構(gòu)的一種 分類 方法。我們這么做的方法是昼激,將一個(gè)給定的結(jié)構(gòu)考慮為一個(gè)更加泛化的基礎(chǔ)定義的具體種類庇绽。
讓我們通過一個(gè)最常被引用的例子來探索這種分類處理。一輛 車 可以被描述為一“類”更泛化的東西——載具——的具體實(shí)現(xiàn)橙困。
我們?cè)谲浖型ㄟ^定義Vehicle
類和Car
類來模型化這種關(guān)系瞧掺。
Vehicle
的定義可能會(huì)包含像動(dòng)力(引擎等),載人能力等等凡傅,這些都是行為辟狈。我們?cè)?code>Vehicle中定義的都是所有(或大多數(shù))不同類型的載具(飛機(jī),火車,機(jī)動(dòng)車)都共同擁有的東西哼转。
在我們的軟件中為每一種不同類型的載具一次又一次地重定義“載人能力”這個(gè)基本性質(zhì)可能沒有道理明未。反而,我們?cè)?code>Vehicle中把這個(gè)能力定義一次壹蔓,之后當(dāng)我們定義Car
時(shí)趟妥,我們簡(jiǎn)單地指出它從基本的Vehicle
定義中“繼承”(或“擴(kuò)展”)。Car
的定義就是特化了一般的Vehicle
定義佣蓉。
雖然Vehicle
和Car
用方法的形式集約地定義了行為披摄,但一個(gè)實(shí)例中的數(shù)據(jù)就像一個(gè)唯一的車牌號(hào)一樣屬于一輛具體的車。
這樣勇凭,類疚膊,繼承,和實(shí)例化就誕生了虾标。
另一個(gè)關(guān)于類的關(guān)鍵概念是“多態(tài)(polymorphism)”寓盗,它描述這樣的想法:一個(gè)來自于父類的泛化行為可以被子類覆蓋,從而使它更加具體璧函。實(shí)際上傀蚌,相對(duì)多態(tài)讓我們?cè)诟采w行為中引用基礎(chǔ)行為。
類理論強(qiáng)烈建議父類和子類對(duì)相同的行為共享同樣的方法名蘸吓,以便于子類(差異化地)覆蓋父類喳张。我們即將看到,在你的JavaScript代碼中這么做會(huì)導(dǎo)致種種困難和脆弱的代碼美澳。
"類(Class)"設(shè)計(jì)模式
你可能從沒把類當(dāng)做一種“設(shè)計(jì)模式”考慮過,因?yàn)樽畛R姷氖顷P(guān)于流行的“面向?qū)ο笤O(shè)計(jì)模式”的討論摸航,比如“迭代器(Iterator)”制跟,“觀察者(Observer)”,“工廠(Factory)”酱虎,“單例(Singleton)”等等雨膨。當(dāng)以這種方式表現(xiàn)時(shí),幾乎可以假定OO的類是我們實(shí)現(xiàn)所有(高級(jí))設(shè)計(jì)模式的底層機(jī)制读串,好像對(duì)所有代碼來說OO是一個(gè)給定的基礎(chǔ)聊记。
取決于你在編程方面接受過的正規(guī)教育的水平,你可能聽說過“過程式編程(procedural programming)”:一種不用任何高級(jí)抽象恢暖,僅僅由過程(也就是函數(shù))調(diào)用其他函數(shù)來構(gòu)成的描述代碼的方式排监。你可能被告知過,類是一個(gè)將過程式風(fēng)格的“面條代碼”轉(zhuǎn)換為結(jié)構(gòu)良好杰捂,組織良好代碼的 恰當(dāng) 的方法舆床。
當(dāng)然,如果你有“函數(shù)式編程(functional programming)”的經(jīng)驗(yàn),你可能知道類只是幾種常見設(shè)計(jì)模式中的一種挨队。但是對(duì)于其他人來說谷暮,這可能是第一次你問自己,類是否真的是代碼的根本基礎(chǔ)盛垦,或者它們是在代碼頂層上的選擇性抽象湿弦。
有些語(yǔ)言(比如Java)不給你選擇,所以這根本沒什么 選擇性——一切都是類腾夯。其他語(yǔ)言如C/C++或PHP同時(shí)給你過程式和面向類的語(yǔ)法颊埃,在使用哪種風(fēng)格合適或混合風(fēng)格上,留給開發(fā)者更多選擇俯在。
JavaScript的“類”
在這個(gè)問題上JavaScript屬于哪一邊竟秫?JS擁有 一些 像類的語(yǔ)法元素(比如new
和instanceof
)有一陣子了,而且在最近的ES6中跷乐,還有一些追加的肥败,比如class
關(guān)鍵字(見附錄A)。
但這意味著JavaScript實(shí)際上 擁有 類嗎愕提?直白且簡(jiǎn)單:沒有馒稍。
由于類是一種設(shè)計(jì)模式,你 可以浅侨,用相當(dāng)?shù)呐Γㄎ覀儗⒃诒菊率O碌牟糠挚吹剑┡耍茖?shí)現(xiàn)很多經(jīng)典類的功能。JS在通過提供看起來像類的語(yǔ)法如输,來努力滿足用類進(jìn)行設(shè)計(jì)的極其廣泛的渴望鼓黔。
雖然我們好像有了看起來像類的語(yǔ)法,但是好像JavaScript機(jī)制在抵抗你使用 類設(shè)計(jì)模式不见,因?yàn)樵诘讓影幕@些你正在上面工作的機(jī)制運(yùn)行的十分不同。語(yǔ)法糖和(極其廣泛被使用的)JS“Class”庫(kù)廢了很大力氣來把這些真實(shí)情況對(duì)你隱藏起來稳吮,但你遲早會(huì)面對(duì)現(xiàn)實(shí):你在其他語(yǔ)言中遇到的 類 和你在JS中模擬的“類”不同缎谷。
總而言之,類是軟件設(shè)計(jì)中的一種可選模式灶似,你可以選擇在JavaScript中使用或不使用它列林。因?yàn)樵S多開發(fā)者都對(duì)面向類的軟件設(shè)計(jì)情有獨(dú)鐘,我們將在本章剩下的部分中探索一下酪惭,為了使用JS提供的東西維護(hù)類的幻覺要付出什么代價(jià)希痴,和我們經(jīng)歷的痛苦。
Class 機(jī)制
在許多面向類語(yǔ)言中撞蚕,“標(biāo)準(zhǔn)庫(kù)”都提供一個(gè)叫“椚筇荩”(壓棧,彈出等)的數(shù)據(jù)結(jié)構(gòu),用一個(gè)Stack
類表示纺铭。這個(gè)類擁有一組變量來存儲(chǔ)數(shù)據(jù)寇钉,還擁有一組可公開訪問的行為(“方法”),這些行為使你的代碼有能力與(隱藏的)數(shù)據(jù)互動(dòng)(添加或移除數(shù)據(jù)等等)舶赔。
但是在這樣的語(yǔ)言中扫倡,你不是直接在Stack
上操作(除非制造一個(gè) 靜態(tài)的 類成員引用,但這超出了我們要討論的范圍)竟纳。Stack
類僅僅是 任何 的“椖炖#”都會(huì)做的事情的一個(gè)抽象解釋,但它本身不是一個(gè)“椬独郏”缘挑。為了得到一個(gè)可以對(duì)之進(jìn)行操作的實(shí)在的數(shù)據(jù)結(jié)構(gòu),你必須 實(shí)例化 這個(gè)Stack
類桶略。
建筑物
傳統(tǒng)的"類(class)"和"實(shí)例(instance)"的比擬源自于建筑物的建造语淘。
一個(gè)建筑師會(huì)規(guī)劃出一棟建筑的所有性質(zhì):多寬,多高际歼,在哪里有多少窗戶惶翻,甚至墻壁和天花板用什么材料。在這個(gè)時(shí)候鹅心,她并不關(guān)心建筑物將會(huì)被建造在 哪里吕粗,她也不關(guān)心有 多少 這棟建筑的拷貝將被建造。
同時(shí)她也不關(guān)心這棟建筑的內(nèi)容——家具旭愧,墻紙颅筋,吊扇等等——她僅關(guān)心建筑物含有何種結(jié)構(gòu)。
她生產(chǎn)的建筑學(xué)上的藍(lán)圖僅僅是建筑物的“方案”输枯。它們不實(shí)際構(gòu)成我們可以實(shí)在進(jìn)入其中并坐下的建筑物垃沦。為了這個(gè)任務(wù)我們需要一個(gè)建筑工。建筑工會(huì)拿走方案并精確地依照它們 建造 這棟建筑物用押。在真正的意義上,他是在將方案中意圖的性質(zhì) 拷貝 到物理建筑物中靶剑。
一旦完成蜻拨,這棟建筑就是藍(lán)圖方案的一個(gè)物理實(shí)例,一個(gè)有望是實(shí)質(zhì)完美的 拷貝桩引。然后建筑工就可以移動(dòng)到隔壁將它再重做一遍缎讼,建造另一個(gè) 拷貝。
建筑物與藍(lán)圖間的關(guān)系是間接的坑匠。你可以檢視藍(lán)圖來了解建筑物是如何構(gòu)造的血崭,但對(duì)于直接考察建筑物的每一部分,僅有藍(lán)圖是不夠的。如果你想打開一扇門夹纫,你不得不走進(jìn)建筑物自身——藍(lán)圖僅僅是為了用來 表示 門的位置而在紙上畫的線條咽瓷。
一個(gè)類就是一個(gè)藍(lán)圖。為了實(shí)際得到一個(gè)對(duì)象并與之互動(dòng)舰讹,我們必須從類中建造(也就是實(shí)例化)某些東西茅姜。這種“構(gòu)建”的最終結(jié)果是一個(gè)對(duì)象,典型地稱為一個(gè)“實(shí)例”月匣,我們可以按需要直接調(diào)用它的方法钻洒,訪問它的公共數(shù)據(jù)屬性。
這個(gè)對(duì)象是所有在類中被描述的特性的 拷貝锄开。
你不太指望走進(jìn)一棟建筑之后發(fā)現(xiàn)素标,一份用于規(guī)劃這棟建筑物的藍(lán)圖被裱起來掛在墻上,雖然藍(lán)圖可能在辦公室的公共記錄的文件中萍悴。相似地头遭,你一般不會(huì)使用對(duì)象實(shí)例來直接訪問和操作類,但是這至少對(duì)于判定對(duì)象實(shí)例來自于 哪個(gè)類 是可能的退腥。
與考慮對(duì)象實(shí)例與它源自的類的任何間接關(guān)系相比任岸,考慮類和對(duì)象實(shí)例的直接關(guān)系更有用。一個(gè)類通過拷貝操作被實(shí)例化為對(duì)象的形式狡刘。
[圖片上傳失敗...(image-d54d08-1515410907270)]
如你所見享潜,箭頭由左向右,從上至下嗅蔬,這表示著概念上和物理上發(fā)生的拷貝操作剑按。
構(gòu)造器(Constructor)
類的實(shí)例由類的一種特殊方法構(gòu)建,這個(gè)方法的名稱通常與類名相同澜术,稱為 “構(gòu)造器(constructor)”艺蝴。這個(gè)方法的明確的工作,就是初始化實(shí)例所需的所有信息(狀態(tài))鸟废。
比如猜敢,考慮下面這個(gè)類的假想代碼(語(yǔ)法是自創(chuàng)的):
class CoolGuy {
specialTrick = nothing
CoolGuy( trick ) {
specialTrick = trick
}
showOff() {
output( "Here's my trick: ", specialTrick )
}
}
為了 制造 一個(gè)CoolGuy
實(shí)例,我們需要調(diào)用類的構(gòu)造器:
Joe = new CoolGuy( "jumping rope" )
Joe.showOff() // Here's my trick: jumping rope
注意盒延,CoolGuy
類有一個(gè)構(gòu)造器CoolGuy()
缩擂,它實(shí)際上就是在我們說new CoolGuy(..)
時(shí)調(diào)用的。我們從這個(gè)構(gòu)造器拿回一個(gè)對(duì)象(類的一個(gè)實(shí)例)添寺,我們可以調(diào)用showOff()
方法胯盯,來打印這個(gè)特定的CoolGuy
的特殊才藝。
顯然计露,跳繩使Joe看起來很酷博脑。
類的構(gòu)造器 屬于 那個(gè)類憎乙,幾乎總是和類同名。同時(shí)叉趣,構(gòu)造器大多數(shù)情況下總是需要用new
來調(diào)用泞边,以便使語(yǔ)言的引擎知道你想要構(gòu)建一個(gè) 新的 類的實(shí)例。
類繼承(Class Inheritance)
在面向類的語(yǔ)言中君账,你不僅可以定義一個(gè)可以初始化它自己的類繁堡,你還可以定義另外一個(gè)類 繼承 自第一個(gè)類。
這第二個(gè)類通常被稱為“子類”乡数,而第一個(gè)類被稱為“父類”椭蹄。這些名詞明顯地來自于親子關(guān)系的比擬,雖然這種比擬有些扭曲净赴,就像你馬上要看到的绳矩。
當(dāng)一個(gè)家長(zhǎng)擁有一個(gè)和他有血緣關(guān)系的孩子時(shí),家長(zhǎng)的遺傳性質(zhì)會(huì)被拷貝到孩子身上玖翅。明顯地翼馆,在大多數(shù)生物繁殖系統(tǒng)中,雙親都平等地貢獻(xiàn)基因進(jìn)行混合金度。但是為了這個(gè)比擬的目的应媚,我們假設(shè)只有一個(gè)親人。
一旦孩子出現(xiàn)猜极,他或她就從親人那里分離出來中姜。這個(gè)孩子受其親人的繼承因素的嚴(yán)重影響,但是獨(dú)一無二跟伏。如果這個(gè)孩子擁有紅色的頭發(fā)丢胚,這并不意味這他的親人的頭發(fā) 曾經(jīng) 是紅色,或者會(huì)自動(dòng) 變成 紅色受扳。
以相似的方式携龟,一旦一個(gè)子類被定義,它就分離且區(qū)別于父類勘高。子類含有一份從父類那里得來的行為的初始拷貝峡蟋,但它可以覆蓋這些繼承的行為,甚至是定義新行為华望。
重要的是层亿,要記住我們?cè)谟懻摳?類 和子 類,而不是物理上的東西立美。這就是這個(gè)親子比擬讓人糊涂的地方,因?yàn)槲覀儗?shí)際上應(yīng)當(dāng)說父類就是親人的DNA方灾,而子類就是孩子的DNA建蹄。我們不得不從兩套DNA制造出(也就是初始化)人碌更,用得到的物理上存在的人來與之進(jìn)行談話。
讓我們把生物學(xué)上的親子放在一邊洞慎,通過一個(gè)稍稍不同的角度來看看繼承:不同種類型的載具痛单。這是用來理解繼承的最經(jīng)典(也是爭(zhēng)議不斷的)的比擬。
讓我們重新審視本章前面的Vehicle
和Car
的討論劲腿⌒袢蓿考慮下面表達(dá)繼承的類的假想代碼:
class Vehicle {
engines = 1
ignition() {
output( "Turning on my engine." )
}
drive() {
ignition()
output( "Steering and moving forward!" )
}
}
class Car inherits Vehicle {
wheels = 4
drive() {
inherited:drive()
output( "Rolling on all ", wheels, " wheels!" )
}
}
class SpeedBoat inherits Vehicle {
engines = 2
ignition() {
output( "Turning on my ", engines, " engines." )
}
pilot() {
inherited:drive()
output( "Speeding through the water with ease!" )
}
}
注意: 為了簡(jiǎn)潔明了,這些類的構(gòu)造器被省略了焦人。
我們定義Vehicle
類挥吵,假定它有一個(gè)引擎,有一個(gè)打開打火器的方法花椭,和一個(gè)行駛的方法忽匈。但你永遠(yuǎn)也不會(huì)制造一個(gè)泛化的“載具”,所以在這里它只是一個(gè)概念的抽象矿辽。
然后我們定義了兩種具體的載具:Car
和SpeedBoat
丹允。它們都繼承Vehicle
的泛化性質(zhì),但之后它們都對(duì)這些性質(zhì)進(jìn)行了合適的特化袋倔。一輛車有4個(gè)輪子雕蔽,一艘快艇有兩個(gè)引擎,意味著它需要在打火時(shí)需要特別注意要啟動(dòng)兩個(gè)引擎宾娜。
多態(tài)(Polymorphism)
Car
定義了自己的drive()
方法批狐,它覆蓋了從Vehicle
繼承來的同名方法。但是碳默,Car
的drive()
方法調(diào)用了inherited:drive()
贾陷,這表示Car
可以引用它繼承的,覆蓋之前的原版drive()
嘱根。SpeedBoat
的pilot()
方法也引用了它繼承的drive()
拷貝髓废。
這種技術(shù)稱為“多態(tài)(polymorphism)”,或“虛擬多態(tài)(virtual polymorphism)”该抒。對(duì)我們當(dāng)前的情況更具體一些慌洪,我們稱之為“相對(duì)多態(tài)(relative polymorphism)”。
多態(tài)這個(gè)話題比我們可以在這里談到的內(nèi)容要寬泛的多凑保,但我們當(dāng)前的“相對(duì)”意味著一個(gè)特殊層面:任何方法都可以引用位于繼承層級(jí)上更高一層的其他方法(同名或不同名)冈爹。我們說“相對(duì)”,因?yàn)槲覀儾唤^對(duì)定義我們想訪問繼承的哪一層(也就是類)欧引,而實(shí)質(zhì)上在說“向上一層”來相對(duì)地引用频伤。
在許多語(yǔ)言中,在這個(gè)例子中使用inherited:
的地方使用了super
關(guān)鍵字芝此,它的基于這樣的想法:一個(gè)“超類(super class)”是當(dāng)前類的父親/祖先憋肖。
多態(tài)的另一個(gè)方面是因痛,一個(gè)方法名可以在繼承鏈的不同層面上有多種定義,而且在解析哪個(gè)方法在被調(diào)用時(shí)岸更,這些定義可以適當(dāng)?shù)乇蛔詣?dòng)選擇鸵膏。
在我們上面的例子中,我們看到這種行為發(fā)生了兩次:drive()
在Vehicle
和Car
中定義, 而ignition()
在Vehicle
和SpeedBoat
中定義怎炊。
注意: 另一個(gè)傳統(tǒng)面向類語(yǔ)言通過super
給你的能力谭企,是從子類的構(gòu)造器中直接訪問父類構(gòu)造器。這很大程度上是對(duì)的评肆,因?yàn)閷?duì)真正的類來說债查,構(gòu)造器屬于這個(gè)類。然而在JS中糟港,這是相反的——實(shí)際上認(rèn)為“類”屬于構(gòu)造器(Foo.prototype...
類型引用)更恰當(dāng)攀操。因?yàn)樵贘S中,父子關(guān)系僅存在于它們各自的構(gòu)造器的兩個(gè).prototype
對(duì)象間秸抚,構(gòu)造器本身不直接關(guān)聯(lián)速和,而且沒有簡(jiǎn)單的方法從一個(gè)中相對(duì)引用另一個(gè)(參見附錄A,看看ES6中用super
“解決”此問題的class
)剥汤。
可以從ignition()
中具體看出多態(tài)的一個(gè)有趣的含義颠放。在pilot()
內(nèi)部,一個(gè)相對(duì)多態(tài)引用指向了(繼承的)Vehicle
版本的drive()
吭敢。而這個(gè)drive()
僅僅通過名稱(不是相對(duì)引用)來引用ignition()
方法碰凶。
語(yǔ)言的引擎會(huì)使用哪一個(gè)版本的ignition()
?是Vehicle
的還是SpeedBoat
的鹿驼?它會(huì)使用SpeedBoat
版本的ignition()
欲低。 如果你 能 初始化Vehicle
類自身,并且調(diào)用它的drive()
畜晰,那么語(yǔ)言引擎將會(huì)使用Vehicle
的ignition()
定義砾莱。
換句話說,ignition()
方法的定義凄鼻,根據(jù)你引用的實(shí)例是哪個(gè)類(繼承層級(jí))而 多態(tài)(改變)腊瑟。
這看起來過于深入學(xué)術(shù)細(xì)節(jié)了。不過為了好好地與JavaScript的[[Prototype]]
機(jī)制的類似行為進(jìn)行對(duì)比块蚌,理解這些細(xì)節(jié)還是很重要的闰非。
如果類是繼承而來的,對(duì)這些類本身(不是由它們創(chuàng)建的對(duì)象)有一個(gè)方法可以 相對(duì)地 引用它們繼承的對(duì)象峭范,這個(gè)相對(duì)引用通常稱為super
财松。
記得剛才這幅圖:
[圖片上傳失敗...(image-c7ea0e-1515410907270)]
注意對(duì)于實(shí)例化(a1
,a2
纱控,b1
辆毡,和b2
) 和 繼承(Bar
)政敢,箭頭如何表示拷貝操作。
從概念上講胚迫,看起來子類Bar
可以使用相對(duì)多態(tài)引用(也就是super
)來訪問它的父類Foo
的行為。然而在現(xiàn)實(shí)中唾那,子類不過是被給與了一份它從父類繼承來的行為的拷貝而已访锻。如果子類“覆蓋”一個(gè)它繼承的方法,原版的方法和覆蓋版的方法實(shí)際上都是存在的闹获,所以它們都是可以訪問的期犬。
不要讓多態(tài)把你搞糊涂,使你認(rèn)為子類是鏈接到父類上的避诽。子類得到一份它需要從父類繼承的東西的拷貝龟虎。類繼承意味著拷貝。
多重繼承(Multiple Inheritance)
能回想起我們?cè)缦忍岬降挠H子和DNA嗎沙庐?我們說過這個(gè)比擬有些奇怪鲤妥,因?yàn)樯飳W(xué)上大多數(shù)后代來自于雙親。如果類可以繼承自其他兩個(gè)類拱雏,那么這個(gè)親子比擬會(huì)更合適一些棉安。
有些面向類的語(yǔ)言允許你指定一個(gè)以上的“父類”來進(jìn)行“繼承”。多重繼承意味著每個(gè)父類的定義都被拷貝到子類中铸抑。
表面上看來贡耽,這是對(duì)面向類的一個(gè)強(qiáng)大的加成能力,給我們能力去將更多功能組合在一起鹊汛。然而蒲赂,這無疑會(huì)產(chǎn)生一些復(fù)雜的問題。如果兩個(gè)父類都提供了名為drive()
的方法刁憋,在子類中的drive()
引用將會(huì)解析為哪個(gè)版本滥嘴?你會(huì)總是不得不手動(dòng)指明哪個(gè)父類的drive()
是你想要的,從而失去一些多態(tài)繼承的優(yōu)雅之處嗎职祷?
還有另外一個(gè)所謂的“鉆石問題”:子類“D”繼承自兩個(gè)父類(“B”和“C”)氏涩,它們兩個(gè)又繼承自共通的父類“A”。如果“A”提供了方法drive()
有梆,而“B”和“C”都覆蓋(多態(tài)地)了這個(gè)方法是尖,那么當(dāng)“D”引用drive()
時(shí),它應(yīng)當(dāng)使用那個(gè)版本呢(B:drive()
還是C:drive()
)泥耀?
[圖片上傳失敗...(image-909515-1515410907270)]
事情會(huì)比我們這樣窺豹一斑能看到的復(fù)雜得多饺汹。我們?cè)谶@里把它們記下來,以便于我們可以將它和JavaScript機(jī)制的工作方式比較痰催。
JavaScript更簡(jiǎn)單:它不為“多重繼承”提供原生機(jī)制兜辞。許多人認(rèn)為這是好事迎瞧,因?yàn)槭∪サ膹?fù)雜性要比“減少”的功能多得多。但是這并不能阻擋開發(fā)者們用各種方法來模擬它逸吵,我們接下來就看看凶硅。
Mixins(混合)
當(dāng)你“繼承”或是“實(shí)例化”時(shí),JavaScript的對(duì)象機(jī)制不會(huì) 自動(dòng)地 執(zhí)行拷貝行為扫皱。很簡(jiǎn)單足绅,在JavaScript中沒有“類”可以拿來實(shí)例化,只有對(duì)象韩脑。而且對(duì)象也不會(huì)被拷貝到另一個(gè)對(duì)象中氢妈,而是被 鏈接在一起(詳見第五章)。
因?yàn)樵谄渌Z(yǔ)言中觀察到的類的行為意味著拷貝段多,讓我們來看看JS開發(fā)者如何在JavaScript中 模擬 這種 缺失 的類的拷貝行為:mixins(混合)首量。我們會(huì)看到兩種“mixin”:明確的(explicit) 和 隱含的(implicit)。
明確的 Mixins(Explicit Mixins)
讓我們?cè)俅位仡櫱懊娴?code>Vehicle和Car
的例子进苍。因?yàn)镴avaScript不會(huì)自動(dòng)地將行為從Vehicle
拷貝到Car
加缘,我們可以建造一個(gè)工具來手動(dòng)拷貝。這樣的工具經(jīng)常被許多包/框架稱為extend(..)
琅捏,但為了說明的目的生百,我們?cè)谶@里叫它mixin(..)
。
// 大幅簡(jiǎn)化的`mixin(..)`示例:
function mixin( sourceObj, targetObj ) {
for (var key in sourceObj) {
// 僅拷貝非既存內(nèi)容
if (!(key in targetObj)) {
targetObj[key] = sourceObj[key];
}
}
return targetObj;
}
var Vehicle = {
engines: 1,
ignition: function() {
console.log( "Turning on my engine." );
},
drive: function() {
this.ignition();
console.log( "Steering and moving forward!" );
}
};
var Car = mixin( Vehicle, {
wheels: 4,
drive: function() {
Vehicle.drive.call( this );
console.log( "Rolling on all " + this.wheels + " wheels!" );
}
} );
注意: 重要的細(xì)節(jié):我們談?wù)摰牟辉偈穷惐樱驗(yàn)樵贘avaScript中沒有類蚀浆。Vehicle
和Car
分別只是我們實(shí)施拷貝的源和目標(biāo)對(duì)象。
Car
現(xiàn)在擁有了一份從Vehicle
得到的屬性和函數(shù)的拷貝搜吧。技術(shù)上講市俊,函數(shù)實(shí)際上沒有被復(fù)制,而是指向函數(shù)的 引用 被復(fù)制了滤奈。所以摆昧,Car
現(xiàn)在有一個(gè)稱為ignition
的屬性,它是一個(gè)ignition()
函數(shù)引用的拷貝蜒程;而且它還有一個(gè)稱為engines
的屬性绅你,持有從Vehicle
拷貝來的值1
。
Car
已經(jīng) 有了drive
屬性(函數(shù))昭躺,所以這個(gè)屬性引用沒有被覆蓋(參見上面mixin(..)
的if
語(yǔ)句)忌锯。
重溫"多態(tài)(Polymorphism)"
我們來考察一下這個(gè)語(yǔ)句:Vehicle.drive.call( this )
。我將之稱為“顯式假想多態(tài)(explicit pseudo-polymorphism)”领炫∨伎澹回想我們前一段假想代碼的這一行是我們稱之為“相對(duì)多態(tài)(relative polymorphism)”的inherited:drive()
。
JavaScript沒有能力實(shí)現(xiàn)相對(duì)多態(tài)(ES6之前,見附錄A)似舵。所以脚猾,因?yàn)?code>Car和Vehicle
都有一個(gè)名為drive()
的函數(shù),為了在它們之間區(qū)別調(diào)用砚哗,我們必須使用絕對(duì)(不是相對(duì))引用龙助。我們明確地用名稱指出Vehicle
對(duì)象,然后在它上面調(diào)用drive()
函數(shù)蛛芥。
但如果我們說Vehicle.drive()
泌参,那么這個(gè)函數(shù)調(diào)用的this
綁定將會(huì)是Vehicle
對(duì)象,而不是Car
對(duì)象(見第二章)常空,那不是我們想要的。所以盖溺,我們使用.call( this )
(見第二章)來保證drive()
在Car
對(duì)象的環(huán)境中被執(zhí)行漓糙。
注意: 如果Car.drive()
的函數(shù)名稱標(biāo)識(shí)符沒有與Vehicle.drive()
的重疊(也就是“遮蔽”;見第五章)烘嘱,我們就不會(huì)有機(jī)會(huì)演示“方法多態(tài)(method polymorphism)”昆禽。因?yàn)槟菢拥脑挘粋€(gè)指向Vehicle.drive()
的引用會(huì)被mixin(..)
調(diào)用拷貝蝇庭,而我們可以使用this.drive()
直接訪問它醉鳖。被選用的標(biāo)識(shí)符重疊 遮蔽 就是為什么我們不得不使用更復(fù)雜的 顯式假想多態(tài)(explicit pseudo-polymorphism) 的原因哮内。
在擁有相對(duì)多態(tài)的面向類的語(yǔ)言中,Car
和Vehicle
間的連接被建立一次北发,就在類定義的頂端,這里是維護(hù)這種關(guān)系的唯一場(chǎng)所琳拨。
但是由于JavaScript的特殊性,顯式假想多態(tài)(因?yàn)檎诒危狱庇。?在每一個(gè)你需要這種(假想)多態(tài)引用的函數(shù)中 建立了一種脆弱的手動(dòng)/顯式鏈接惊畏。這可能會(huì)顯著地增加維護(hù)成本。而且密任,雖然顯式假想多態(tài)可以模擬“多重繼承”的行為颜启,但這只會(huì)增加復(fù)雜性和代碼脆弱性。
這種方法的結(jié)果通常是更加復(fù)雜批什,更難讀懂农曲,而且 更難維護(hù)的代碼。應(yīng)當(dāng)盡可能地避免使用顯式假想多態(tài),因?yàn)樵诖蟛糠謱用嫔纤拇鷥r(jià)要高于利益乳规。
混合拷貝(Mixing Copies)
回憶上面的mixin(..)
工具:
// 大幅簡(jiǎn)化的`mixin()`示例:
function mixin( sourceObj, targetObj ) {
for (var key in sourceObj) {
// 僅拷貝不存在的屬性
if (!(key in targetObj)) {
targetObj[key] = sourceObj[key];
}
}
return targetObj;
}
現(xiàn)在形葬,我們考察一下mixin(..)
如何工作澎粟。它迭代sourceObj
(在我們的例子中是Vehicle
)的所有屬性啥辨,如果在targetObj
(在我們的例子中是Car
)中沒有名稱與之匹配的屬性,它就進(jìn)行拷貝缩搅。因?yàn)槲覀兪窃诔跏紝?duì)象存在的情況下進(jìn)行拷貝冻辩,所以我們要小心不要將目標(biāo)屬性覆蓋掉猖腕。
如果在指明Car
的具體內(nèi)容之前,我們先進(jìn)行拷貝恨闪,那么我們就可以省略對(duì)targetObj
檢查倘感,但是這樣做有些笨拙且低效,所以通常不優(yōu)先選用:
// 另一種mixin咙咽,對(duì)覆蓋不太“安全”
function mixin( sourceObj, targetObj ) {
for (var key in sourceObj) {
targetObj[key] = sourceObj[key];
}
return targetObj;
}
var Vehicle = {
// ...
};
// 首先老玛,創(chuàng)建一個(gè)空對(duì)象
// 將Vehicle的內(nèi)容拷貝進(jìn)去
var Car = mixin( Vehicle, { } );
// 現(xiàn)在拷貝Car的具體內(nèi)容
mixin( {
wheels: 4,
drive: function() {
// ...
}
}, Car );
不論哪種方法,我們都顯式地將Vehicle
中的非重疊內(nèi)容拷貝到Car
中钧敞±“mixin”這個(gè)名稱來自于解釋這個(gè)任務(wù)的另一種方法:Car
混入Vehicle
的內(nèi)容,就像你吧巧克力碎片混入你最喜歡的曲奇餅面團(tuán)溉苛。
這個(gè)拷貝操作的結(jié)果,是Car
將會(huì)獨(dú)立于Vehicle
運(yùn)行愚战。如果你在Car
上添加屬性寂玲,它不會(huì)影響到Vehicle
,反之亦然佑淀。
注意: 這里有幾個(gè)小細(xì)節(jié)被忽略了伸刃。仍然有一些微妙的方法使兩個(gè)對(duì)象在拷貝完成后還能互相“影響”對(duì)方捧颅,比如他們共享一個(gè)共通對(duì)象(比如數(shù)組)的引用较雕。
由于兩個(gè)對(duì)象還共享它們的共通函數(shù)的引用,這意味著 即便手動(dòng)將函數(shù)從一個(gè)對(duì)象拷貝(也就是混入)到另一個(gè)對(duì)象中扣典,也不能 實(shí)際上模擬 發(fā)生在面向類的語(yǔ)言中的從類到實(shí)例的真正的復(fù)制贮尖。
JavaScript函數(shù)不能真正意義上地被復(fù)制(以標(biāo)準(zhǔn)湿硝,可靠的方式),所以你最終得到的是同一個(gè)共享的函數(shù)對(duì)象(函數(shù)是對(duì)象示括;見第三章)的 被復(fù)制的引用例诀。舉例來說,如果你在一個(gè)共享的函數(shù)對(duì)象(比如ignition()
)上添加屬性來修改它拱她,Vehicle
和Car
都會(huì)通過這個(gè)共享的引用而受影響秉沼。
在JavaScript中明確的mixin是一種不錯(cuò)的機(jī)制唬复。但是它們顯得言過其實(shí)。和將一個(gè)屬性定義兩次相比棘捣,將屬性從一個(gè)對(duì)象拷貝到另一個(gè)對(duì)象并不會(huì)產(chǎn)生多少 實(shí)際的 好處乍恐。這對(duì)我們剛才提到的給函數(shù)對(duì)象引用增加的微妙變化來說茵烈,顯得尤為正確呜投。
如果你明確地將兩個(gè)或更多對(duì)象混入你的目標(biāo)對(duì)象,你可以 某種程度上模擬 “多重繼承”的行為雕拼,但是在將方法或?qū)傩詮亩嘤谝粋€(gè)源對(duì)象那里拷貝過來時(shí)悲没,沒有直接的辦法可以解決名稱的沖突示姿。有些開發(fā)者/包使用“l(fā)ate binding(延遲綁定)”和其他詭異的替代方法來解決問題逊笆,但從根本上講难裆,這些“技巧” 通常 得不償失(而且低效D烁辍)。
要小心的是缩歪,僅在明確的mixin能夠?qū)嶋H提高代碼可讀性時(shí)使用它匪蝙,而如果你發(fā)現(xiàn)它使代碼變得更很難追溯逛球,或在對(duì)象間建立了不必要或笨重的依賴性時(shí)苫昌,要避免使用這種模式祟身。
如果正確使用mixin使你的問題變得比以前 困難月而,那么你可能應(yīng)當(dāng)停止使用mixin。實(shí)際上溢谤,如果你不得不使用復(fù)雜的包/工具來處理這些細(xì)節(jié)世杀,這可能標(biāo)志著你正走在更困難,也許沒必要的道路上蛛壳。在第六章中衙荐,我們將試著提取一種更簡(jiǎn)單的方法來實(shí)現(xiàn)我們期望的結(jié)果忧吟,同時(shí)免去這些周折斩披。
寄生繼承(Parasitic Inheritance)
明確的mixin模式的一個(gè)變種垦沉,在某種意義上是明確的而在某種意義上是隱含的,稱為“寄生繼承(Parasitic Inheritance)”寡壮,它主要是由Douglas Crockford推廣的。
這是它如何工作:
// “傳統(tǒng)的JS類” `Vehicle`
function Vehicle() {
this.engines = 1;
}
Vehicle.prototype.ignition = function() {
console.log( "Turning on my engine." );
};
Vehicle.prototype.drive = function() {
this.ignition();
console.log( "Steering and moving forward!" );
};
// “寄生類” `Car`
function Car() {
// 首先, `car`是一個(gè)`Vehicle`
var car = new Vehicle();
// 現(xiàn)在, 我們修改`car`使它特化
car.wheels = 4;
// 保存一個(gè)`Vehicle::drive()`的引用
var vehDrive = car.drive;
// 覆蓋 `Vehicle::drive()`
car.drive = function() {
vehDrive.call( this );
console.log( "Rolling on all " + this.wheels + " wheels!" );
};
return car;
}
var myCar = new Car();
myCar.drive();
// Turning on my engine.
// Steering and moving forward!
// Rolling on all 4 wheels!
如你所見闸婴,我們一開始從“父類”(對(duì)象)Vehicle
制造了一個(gè)定義的拷貝邪乍,之后將我們的“子類”(對(duì)象)定義混入其中(按照需要保留父類的引用)庇楞,最后將組合好的對(duì)象car
作為子類實(shí)例傳遞出去吕晌。
注意: 當(dāng)我們調(diào)用new Car()
時(shí)临燃,一個(gè)新對(duì)象被創(chuàng)建并被Car
的this
所引用(見第二章)。但是由于我們沒有使用這個(gè)對(duì)象乏沸,而是返回我們自己的car
對(duì)象蹬跃,所以這個(gè)初始化創(chuàng)建的對(duì)象就被丟棄了蝶缀。所以扼劈,Car()
可以不用new
關(guān)鍵字調(diào)用,就可以實(shí)現(xiàn)和上面代碼相同的功能骑冗,而且還可以節(jié)省對(duì)象的創(chuàng)建和回收贼涩。
隱含的 Mixin(Implicit Mixins)
隱含的mixin和前面解釋的 顯式假想多態(tài) 是緊密相關(guān)的遥倦。所以它們需要注意相同的事項(xiàng)袒哥。
考慮這段代碼:
var Something = {
cool: function() {
this.greeting = "Hello World";
this.count = this.count ? this.count + 1 : 1;
}
};
Something.cool();
Something.greeting; // "Hello World"
Something.count; // 1
var Another = {
cool: function() {
// 隱式地將`Something`混入`Another`
Something.cool.call( this );
}
};
Another.cool();
Another.greeting; // "Hello World"
Another.count; // 1 (不會(huì)和`Something`共享狀態(tài))
Something.cool.call( this )
既可以在“構(gòu)造器”調(diào)用中使用(最常見的情況)堡称,也可以在方法調(diào)用中使用(如這里所示)艺演,我們實(shí)質(zhì)上“借用”了Something.cool()
函數(shù)并在Another
環(huán)境下胎撤,而非Something
環(huán)境下調(diào)用它(通過this
綁定,見第二章)巫俺。結(jié)果是识藤,Something.cool()
中進(jìn)行的賦值被實(shí)施到了Another
對(duì)象而非Something
對(duì)象。
那么稽穆,這就是說我們將Something
的行為“混入”了Another
舌镶。
雖然這種技術(shù)看起來有效利用了this
再綁定的功能餐胀,也就是生硬地調(diào)用Something.cool.call( this )
否灾,但是這種調(diào)用不能被作為相對(duì)(也更靈活的)引用墨技,所以你應(yīng)當(dāng) 提高警惕扣汪。一般來說崭别,盡量避免使用這種結(jié)構(gòu) 來保持代碼干凈而且容易維護(hù)茅主。
復(fù)習(xí)
類是一種設(shè)計(jì)模式诀姚。許多語(yǔ)言提供語(yǔ)法來啟用自然而然的面向類的軟件設(shè)計(jì)鞭衩。JS也有相似的語(yǔ)法娃善,但是它的行為和你在其他語(yǔ)言中熟悉的工作原理 有很大的不同聚磺。
類意味著拷貝瘫寝。
當(dāng)一個(gè)傳統(tǒng)的類被實(shí)例化時(shí)稠炬,就發(fā)生了類的行為向?qū)嵗锌截愂灼簟.?dāng)類被繼承時(shí)毅桃,也發(fā)生父類的行為向子類的拷貝钥飞。
多態(tài)(在繼承鏈的不同層級(jí)上擁有同名的不同函數(shù))也許看起來意味著一個(gè)從子類回到父類的相對(duì)引用鏈接读宙,但是它仍然只是拷貝行的的結(jié)果结闸。
JavaScript 不會(huì)自動(dòng)地 (像類那樣)在對(duì)象間創(chuàng)建拷貝膀估。
mixin模式常用于在 某種程度上 模擬類的拷貝行為察纯,但是這通常導(dǎo)致像顯式假想多態(tài)那樣(OtherObj.methodName.call(this, ...)
)難看而且脆弱的語(yǔ)法饼记,這樣的語(yǔ)法又常導(dǎo)致更難懂和更難維護(hù)的代碼慰枕。
顯式mixin和類 拷貝 又不完全相同具帮,因?yàn)閷?duì)象(和函數(shù)!)僅僅是共享的引用被復(fù)制匪凡,不是對(duì)象/函數(shù)自身被復(fù)制病游。不注意這樣的微小之處通常是各種陷阱的根源衬衬。
一般來講,在JS中模擬類通常會(huì)比解決當(dāng)前 真正 的問題埋下更多的坑滋尉。