第四章: 混合(淆)“類”的對象

特別說明炊甲,為便于查閱衰腌,文章轉自https://github.com/getify/You-Dont-Know-JS

接著我們上一章對對象的探索飞傀,我們很自然的將注意力轉移到“面向對象(OO)編程”,與“類(class)”舔腾。我們先將“面向類”作為設計模式來看看沪摄,之后我們再考察“類”的機制:“實例化(instantiation)”, “繼承(inheritance)”與“(相對)多態(tài)(relative polymorphism)”躯嫉。

我們將會看到,這些概念并不是非常自然地映射到 JS 的對象機制上杨拐,以及許多 JavaScript 開發(fā)者為了克服這些挑戰(zhàn)所做的努力(mixins等)祈餐。

注意: 這一章花了相當一部分時間(前一半!)在著重解釋“面向對象編程”理論上哄陶。在后半部分討論“Mixins(混合)”時帆阳,我們最終會將這些理論與真實且實際的 JavaScript 代碼聯(lián)系起來。但是這里首先要蹚過許多概念和假想代碼屋吨,所以可別跟丟了 —— 堅持下去蜒谤!

類理論

“類/繼承”描述了一種特定的代碼組織和結構形式 —— 一種在我們的軟件中對真實世界的建模方法。

OO 或者面向類的編程強調數(shù)據(jù)和操作它的行為之間有固有的聯(lián)系(當然至扰,依數(shù)據(jù)的類型和性質不同而不同w⒒铡),所以合理的設計是將數(shù)據(jù)和行為打包在一起(也稱為封裝)敢课。這有時在正式的計算機科學中稱為“數(shù)據(jù)結構”阶祭。

比如绷杜,表示一個單詞或短語的一系列字符通常稱為“string(字符串)”。這些字符就是數(shù)據(jù)濒募。但你幾乎從來不關心數(shù)據(jù)接剩,你總是想對數(shù)據(jù) 做事情, 所以可以 數(shù)據(jù)實施的行為(計算它的長度萨咳,在末尾添加數(shù)據(jù),檢索疫稿,等等)都被設計成為 String 類的方法培他。

任何給定的字符串都是這個類的一個實例,這個類是一個整齊的集合包裝:字符數(shù)據(jù)和我們可以對它進行操作的功能。

類還隱含著對一個特定數(shù)據(jù)結構的一種 分類 方法。其做法是將一個給定的結構考慮為一個更加泛化的基礎定義的具體種類芯丧。

讓我們通過一個最常被引用的例子來探索這種分類處理豪诲。一輛 可以被描述為一“類”更泛化的東西 —— 載具 —— 的具體實現(xiàn)。

我們在軟件中通過定義 Vehicle 類和 Car 類來模型化這種關系祖屏。

Vehicle 的定義可能會包含像動力(引擎等),載人能力等等,這些都是行為懊烤。我們在 Vehicle 中定義的都是所有(或大多數(shù))不同類型的載具(飛機、火車宽堆、機動車)都共同擁有的東西腌紧。

在我們的軟件中為每一種不同類型的載具一次又一次地重定義“載人能力”這個基本性質可能沒有道理。反而畜隶,我們在 Vehicle 中把這個能力定義一次壁肋,之后當我們定義 Car 時,我們簡單地指出它從基本的 Vehicle 定義中“繼承”(或“擴展”)籽慢。于是 Car 的定義就被稱為特化了更一般的 Vehicle 定義浸遗。

VehicleCar 用方法的形式集約地定義了行為,另一方面一個實例中的數(shù)據(jù)就像一個唯一的車牌號一樣屬于一輛具體的車箱亿。

這樣跛锌,類,繼承届惋,和實例化就誕生了察净。

另一個關于類的關鍵概念是“多態(tài)(polymorphism)”,它描述這樣的想法:一個來自于父類的泛化行為可以被子類覆蓋盼樟,從而使它更加具體氢卡。實際上,相對多態(tài)允許我們在覆蓋行為中引用基礎行為晨缴。

類理論強烈建議父類和子類對相同的行為共享同樣的方法名译秦,以便于子類(差異化地)覆蓋父類。我們即將看到,在你的 JavaScript 代碼中這么做會導致種種困難和脆弱的代碼筑悴。

"類"設計模式

你可能從沒把類當做一種“設計模式”考慮過们拙,因為最常見的是關于流行的“面向對象設計模式”的討論,比如“迭代器(Iterator)”阁吝、“觀察者(Observer)”砚婆、“工廠(Factory)”、“單例(Singleton)”等等突勇。當以這種方式表現(xiàn)時装盯,幾乎可以假定 OO 的類是我們實現(xiàn)所有(高級)設計模式的底層機制,好像對所有代碼來說 OO 是一個給定的基礎甲馋。

取決于你在編程方面接受過的正規(guī)教育的水平埂奈,你可能聽說過“過程式編程(procedural programming)”:一種不用任何高級抽象,僅僅由過程(也就是函數(shù))調用其他函數(shù)構成的描述代碼的方式定躏。你可能被告知過账磺,類是一個將過程式風格的“面條代碼”轉換為結構良好,組織良好代碼的 恰當 的方法痊远。

當然垮抗,如果你有“函數(shù)式編程(functional programming)”的經驗,你可能知道類只是幾種常見設計模式中的一種碧聪。但是對于其他人來說借宵,這可能是第一次你問自己,類是否真的是代碼的根本基礎矾削,或者它們是在代碼頂層上的選擇性抽象壤玫。

有些語言(比如 Java)不給你選擇,所以這根本沒什么 選擇性 —— 一切都是類哼凯。其他語言如 C/C++ 或 PHP 同時給你過程式和面向類的語法欲间,在使用哪種風格合適或混合風格上,留給開發(fā)者更多選擇断部。

JavaScript 的“類”

在這個問題上 JavaScript 屬于哪一邊猎贴?JS 擁有 一些 像類的語法元素(比如 newinstanceof)有一陣子了,而且在最近的 ES6 中蝴光,還有一些追加的東西她渴,比如 class 關鍵字(見附錄A)。

但這意味著 JavaScript 實際上 擁有 類嗎蔑祟?簡單明了:沒有趁耗。

由于類是一種設計模式,你 可以疆虚,用相當?shù)呐Γㄕ缥覀儗⒃诒菊率O碌牟糠挚吹降模┛涟埽茖崿F(xiàn)很多經典類的功能满葛。JS 在通過提供看起來像類的語法,來努力滿足用類進行設計的極其廣泛的 渴望罢屈。

雖然我們好像有了看起來像類的語法嘀韧,但是 JavaScript 機制好像在抵抗你使用 類設計模式,因為在底層缠捌,這些你正在上面工作的機制運行的十分不同锄贷。語法糖和(極其廣泛被使用的)JS “Class”庫費了很大力氣來把這些真實情況對你隱藏起來,但你遲早會面對現(xiàn)實:你在其他語言中遇到的 和你在 JS 中模擬的“類”不同曼月。

總而言之谊却,類是軟件設計中的一種可選模式,你可以選擇在 JavaScript 中使用或不使用它十嘿。因為許多開發(fā)者都對面向類的軟件設計情有獨鐘,我們將在本章剩下的部分中探索一下岳锁,為了使用 JS 提供的東西維護類的幻覺要付出什么代價绩衷,和我們經歷的痛苦。

類機制

在許多面向類語言中激率,“標準庫”都提供一個叫“椏妊啵”(壓棧,彈出等)的數(shù)據(jù)結構乒躺,用一個 Stack 類表示招盲。這個類擁有一組變量來存儲數(shù)據(jù),還擁有一組可公開訪問的行為(“方法”)嘉冒,這些行為使你的代碼有能力與(隱藏的)數(shù)據(jù)互動(添加或移除數(shù)據(jù)等等)曹货。

但是在這樣的語言中,你不是直接在 Stack 上操作(除非制造一個 靜態(tài)的 類成員引用讳推,但這超出了我們要討論的范圍)顶籽。Stack 類僅僅是 任何 的“棧”都會做的事情的一個抽象解釋银觅,但它本身不是一個“椑癖ィ”。為了得到一個可以對之進行操作的實在的數(shù)據(jù)結構究驴,你必須 實例化 這個 Stack 類镊绪。

建筑物

傳統(tǒng)的"類(class)"和"實例(instance)"的比擬源自于建筑物的建造。

一個建筑師會規(guī)劃出一棟建筑的所有性質:多寬洒忧,多高蝴韭,在哪里有多少窗戶,甚至墻壁和天花板用什么材料熙侍。在這個時候万皿,她并不關心建筑物將會被建造在 哪里摧找,她也不關心有 多少 這棟建筑的拷貝將被建造。

同時她也不關心這棟建筑的內容 —— 家具牢硅、墻紙蹬耘、吊扇等等 —— 她僅關心建筑物含有何種結構。

她生產的建筑學上的藍圖僅僅是建筑物的“方案”减余。它們不實際構成我們可以實在進入其中并坐下的建筑物综苔。為了這個任務我們需要一個建筑工人。建筑工人會拿走方案并精確地依照它們 建造 這棟建筑物位岔。在真正的意義上如筛,他是在將方案中意圖的性質 拷貝 到物理建筑物中。

一旦完成抒抬,這棟建筑就是藍圖方案的一個物理實例杨刨,一個很可能實質完美的 拷貝。然后建筑工人就可以移動到隔壁將它再重做一遍擦剑,建造另一個 拷貝妖胀。

建筑物與藍圖間的關系是間接的。你可以檢視藍圖來了解建筑物是如何構造的惠勒,但對于直接考察建筑物的每一部分赚抡,僅有藍圖是不夠的。如果你想打開一扇門纠屋,你不得不走進建筑物自身 —— 藍圖僅僅是為了用來 表示 門的位置而在紙上畫的線條涂臣。

一個類就是一個藍圖。為了實際得到一個對象并與之互動售担,我們必須從類中建造(也就是實例化)某些東西赁遗。這種“構建”的最終結果是一個對象,通常稱為一個“實例”族铆,我們可以按需要直接調用它的方法吼和,訪問它的公共數(shù)據(jù)屬性。

這個對象是所有在類中被描述的特性的 拷貝骑素。

你不太可能會指望走進一棟建筑之后發(fā)現(xiàn)炫乓,一份用于規(guī)劃這棟建筑物的藍圖被裱起來掛在墻上,雖然藍圖可能在辦公室的公共記錄的文件中献丑。相似地末捣,你一般不會使用對象實例來直接訪問和操作類,但是對于判定對象實例來自于 哪個類 至少是可能的创橄。

與考慮對象實例與它源自的類的任何間接關系相比箩做,考慮類和對象實例的直接關系更有用。一個類通過拷貝操作被實例化為對象的形式妥畏。

如你所見邦邦,箭頭由左向右安吁,從上至下,這表示著概念上和物理上發(fā)生的拷貝操作燃辖。

構造器(Constructor)

類的實例由類的一種特殊方法構建鬼店,這個方法的名稱通常與類名相同,稱為 “構造器(constructor)”黔龟。這個方法的具體工作妇智,就是初始化實例所需的所有信息(狀態(tài))。

比如氏身,考慮下面這個類的假想代碼(語法是自創(chuàng)的):

class CoolGuy {
    specialTrick = nothing

    CoolGuy( trick ) {
        specialTrick = trick
    }

    showOff() {
        output( "Here's my trick: ", specialTrick )
    }
}

為了 制造 一個 CoolGuy 實例巍棱,我們需要調用類的構造器:

Joe = new CoolGuy( "jumping rope" )

Joe.showOff() // Here's my trick: jumping rope

注意,CoolGuy 類有一個構造器 CoolGuy()蛋欣,它實際上就是在我們說 new CoolGuy(..) 時調用的航徙。我們從這個構造器拿回一個對象(類的一個實例),我們可以調用 showOff() 方法陷虎,來打印這個特定的 CoolGuy 的特殊才藝到踏。

顯然,跳繩使Joe看起來很酷泻红。

類的構造器 屬于 那個類夭禽,幾乎總是和類同名霞掺。同時谊路,構造器大多數(shù)情況下總是需要用 new 來調用,以便使語言的引擎知道你想要構建一個 新的 類的實例菩彬。

類繼承

在面向類的語言中缠劝,你不僅可以定義一個能夠初始化它自己的類,你還可以定義另外一個類 繼承 自第一個類骗灶。

這第二個類通常被稱為“子類”惨恭,而第一個類被稱為“父類”。這些名詞顯然來自于親子關系的比擬耙旦,雖然這種比擬有些扭曲脱羡,就像你馬上要看到的。

當一個家長擁有一個和他有血緣關系的孩子時免都,家長的遺傳性質會被拷貝到孩子身上锉罐。明顯地,在大多數(shù)生物繁殖系統(tǒng)中绕娘,雙親都平等地貢獻基因進行混合脓规。但是為了這個比擬的目的,我們假設只有一個親人险领。

一旦孩子出現(xiàn)侨舆,他或她就從親人那里分離出來秒紧。這個孩子受其親人的繼承因素的嚴重影響,但是獨一無二挨下。如果這個孩子擁有紅色的頭發(fā)熔恢,這并不意味這他的親人的頭發(fā) 曾經 是紅色,或者會自動 變成 紅色复颈。

以相似的方式绩聘,一旦一個子類被定義,它就分離且區(qū)別于父類耗啦。子類含有一份從父類那里得來的行為的初始拷貝凿菩,但它可以覆蓋這些繼承的行為,甚至是定義新行為帜讲。

重要的是衅谷,要記住我們是在討論父 和子 ,而不是物理上的東西似将。這就是這個親子比擬讓人糊涂的地方获黔,因為我們實際上應當說父類就是親人的 DNA,而子類就是孩子的 DNA在验。我們不得不從兩套 DNA 制造出(也就是“初始化”)人玷氏,用得到的物理上存在的人來與之進行談話。

讓我們把生物學上的親子放在一邊腋舌,通過一個稍稍不同的角度來看看繼承:不同種類型的載具盏触。這是用來理解繼承的最經典(也是爭議不斷的)的比擬。

讓我們重新審視本章前面的 VehicleCar 的討論块饺≡薇纾考慮下面表達繼承的類的假想代碼:

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!" )
    }
}

注意: 為了簡潔明了,這些類的構造器被省略了授艰。

我們定義 Vehicle 類辨嗽,假定它有一個引擎,有一個打開打火器的方法淮腾,和一個行駛的方法糟需。但你永遠也不會制造一個泛化的“載具”,所以在這里它只是一個概念的抽象谷朝。

然后我們定義了兩種具體的載具:CarSpeedBoat洲押。它們都繼承 Vehicle 的泛化性質,但之后它們都對這些性質進行了恰當?shù)奶鼗墙R惠v車有4個輪子诅诱,一艘快艇有兩個引擎,意味著它需要在打火時需要特別注意要啟動兩個引擎送朱。

多態(tài)(Polymorphism)

Car 定義了自己的 drive() 方法娘荡,它覆蓋了從 Vehicle 繼承來的同名方法干旁。但是,Cardrive() 方法調用了 inherited:drive()炮沐,這表示 Car 可以引用它繼承的争群,覆蓋之前的原版 drive()SpeedBoatpilot() 方法也引用了它繼承的 drive() 拷貝大年。

這種技術稱為“多態(tài)(polymorphism)”换薄,或“虛擬多態(tài)(virtual polymorphism)”。對我們當前的情況更具體一些翔试,我們稱之為“相對多態(tài)(relative polymorphism)”轻要。

多態(tài)這個話題比我們可以在這里談到的內容要寬泛的多,但我們當前的“相對”意味著一個特殊層面:任何方法都可以引用位于繼承層級上更高一層的其他(同名或不同名的)方法垦缅。我們說“相對”冲泥,因為我們不絕對定義我們想訪問繼承的哪一層(也就是類),而實質上用“向上一層”來相對地引用壁涎。

在許多語言中凡恍,在這個例子中出現(xiàn) inherited: 的地方使用了 super 關鍵字,它基于這樣的想法:一個“超類(super class)”是當前類的父親/祖先怔球。

多態(tài)的另一個方面是嚼酝,一個方法名可以在繼承鏈的不同層級上有多種定義,而且在解析哪個方法在被調用時竟坛,這些定義可以適當?shù)乇蛔詣舆x擇闽巩。

在我們上面的例子中,我們看到這種行為發(fā)生了兩次:drive()VehicleCar 中定義, 而 ignition()VehicleSpeedBoat 中定義流码。

注意: 另一個傳統(tǒng)面向類語言通過 super 給你的能力又官,是從子類的構造器中直接訪問父類構造器延刘。這很大程度上是對的漫试,因為對真正的類來說,構造器屬于這個類碘赖。然而在 JS 中驾荣,這是相反的 —— 實際上認為“類”屬于構造器(Foo.prototype... 類型引用)更恰當。因為在 JS 中普泡,父子關系僅存在于它們各自的構造器的兩個.prototype 對象間播掷,構造器本身不直接關聯(lián),而且沒有簡單的方法從一個中相對引用另一個(參見附錄A撼班,看看 ES6 中用 super “解決”此問題的 class)歧匈。

可以從 ignition() 中具體看出多態(tài)的一個有趣的含義。在 pilot() 內部砰嘁,一個相對多態(tài)引用指向了(被繼承的)Vehicle 版本的 drive()件炉。而這個 drive() 僅僅通過名稱(不是相對引用)來引用 ignition() 方法勘究。

語言的引擎會使用哪一個版本的 ignition()?是 Vehicle 的還是 SpeedBoat 的斟冕?它會使用 SpeedBoat 版本的 ignition()口糕。 如果你 初始化 Vehicle 類自身,并且調用它的 drive()磕蛇,那么語言引擎將會使用 Vehicleignition() 定義景描。

換句話說,ignition() 方法的定義秀撇,根據(jù)你引用的實例是哪個類(繼承層級)而 多態(tài)(改變)超棺。

這看起來過于深入學術細節(jié)了。不過為了好好地與 JavaScript 的 [[Prototype]] 機制的類似行為進行對比呵燕,理解這些細節(jié)還是很重要的说搅。

如果類是繼承而來的,對這些類本身(不是由它們創(chuàng)建的對象B驳取)有一個方法可以 相對地 引用它們繼承的對象弄唧,這個相對引用通常稱為 super

記得剛才這幅圖:

注意對于實例化(a1霍衫、a2候引、b1、和 b2) 繼承(Bar)敦跌,箭頭如何表示拷貝操作澄干。

從概念上講,看起來子類 Bar 可以使用相對多態(tài)引用(也就是 super)來訪問它的父類 Foo 的行為柠傍。然而在現(xiàn)實中麸俘,子類不過是被給與了一份它從父類繼承來的行為的拷貝而已。如果子類“覆蓋”一個它繼承的方法惧笛,原版的方法和覆蓋版的方法實際上都是存在的从媚,所以它們都是可以訪問的。

不要讓多態(tài)把你搞糊涂患整,使你認為子類是鏈接到父類上的拜效。子類得到一份它需要從父類繼承的東西的拷貝。類繼承意味著拷貝各谚。

多重繼承(Multiple Inheritance)

能回想起我們早先提到的親子和 DNA 嗎紧憾?我們說過這個比擬有些奇怪,因為生物學上大多數(shù)后代來自于雙親昌渤。如果類可以繼承自其他兩個類赴穗,那么這個親子比擬會更合適一些。

有些面向類的語言允許你指定一個以上的“父類”來進行“繼承”。多重繼承意味著每個父類的定義都被拷貝到子類中般眉。

表面上看來加矛,這是對面向類的一個強大的加成,給我們能力去將更多功能組合在一起煤篙。然而斟览,這無疑會產生一些復雜的問題。如果兩個父類都提供了名為 drive() 的方法辑奈,在子類中的 drive() 引用將會解析為哪個版本苛茂?你會總是不得不手動指明哪個父類的 drive() 是你想要的,從而失去一些多態(tài)繼承的優(yōu)雅之處嗎鸠窗?

還有另外一個所謂的“鉆石問題”:子類“D”繼承自兩個父類(“B”和“C”)妓羊,它們兩個又繼承自共通的父類“A”。如果“A”提供了方法 drive()稍计,而“B”和“C”都覆蓋(多態(tài)地)了這個方法躁绸,那么當“D”引用 drive() 時,它應當使用那個版本呢(B:drive() 還是 C:drive())臣嚣?

事情會比我們這樣窺豹一斑能看到的復雜得多净刮。我們在這里將它們提出來,只是便于我們可以將它和 JavaScript 機制的工作方式比較硅则。

JavaScript 更簡單:它不為“多重繼承”提供原生機制淹父。許多人認為這是好事,因為省去的復雜性要比“減少”的功能多得多怎虫。但是這并不能阻擋開發(fā)者們用各種方法來模擬它暑认,我們接下來就看看。

混合(Mixin)

當你“繼承”或是“實例化”時大审,JavaScript 的對象機制不會 自動地 執(zhí)行拷貝行為蘸际。很簡單,在 JavaScript 中沒有“類”可以拿來實例化徒扶,只有對象粮彤。而且對象也不會被拷貝到另一個對象中,而是被 鏈接在一起(詳見第五章)酷愧。

因為在其他語言中觀察到的類的行為意味著拷貝驾诈,讓我們來看看 JS 開發(fā)者如何在 JavaScript 中 模擬 這種 缺失 的類的拷貝行為:mixins(混合)缠诅。我們會看到兩種“mixin”:明確的(explicit)隱含的(implicit)溶浴。

明確的 Mixin(Explicit Mixins)

讓我們再次回顧前面的 VehicleCar 的例子。因為 JavaScript 不會自動地將行為從 Vehicle 拷貝到 Car管引,我們可以建造一個工具來手動拷貝士败。這樣的工具經常被許多庫/框架稱為 extend(..),但為了便于說明,我們在這里叫它 mixin(..)谅将。

// 大幅簡化的 `mixin(..)` 示例:
function mixin( sourceObj, targetObj ) {
    for (var key in sourceObj) {
        // 僅拷貝非既存內容
        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!" );
    }
} );

注意: 重要的細節(jié):我們談論的不再是類漾狼,因為在 JavaScript 中沒有類。VehicleCar 分別只是我們實施拷貝的源和目標對象饥臂。

Car 現(xiàn)在擁有了一份從 Vehicle 得到的屬性和函數(shù)的拷貝逊躁。技術上講,函數(shù)實際上沒有被復制隅熙,而是指向函數(shù)的 引用 被復制了稽煤。所以,Car 現(xiàn)在有一個稱為 ignition 的屬性囚戚,它是一個 ignition() 函數(shù)引用的拷貝酵熙;而且它還有一個稱為 engines 的屬性,持有從 Vehicle 拷貝來的值 1驰坊。

Car已經 有了 drive 屬性(函數(shù))匾二,所以這個屬性引用沒有被覆蓋(參見上面 mixin(..)if 語句)。

重溫"多態(tài)(Polymorphism)"

我們來考察一下這個語句:Vehicle.drive.call( this )拳芙。我將之稱為“顯式假想多態(tài)(explicit pseudo-polymorphism)”察藐。回想一下舟扎,我們前一段假想代碼的這一行是我們稱之為“相對多態(tài)(relative polymorphism)”的 inherited:drive()转培。

JavaScript 沒有能力實現(xiàn)相對多態(tài)(ES6 之前,見附錄A)浆竭。所以浸须,因為 CarVehicle 都有一個名為 drive() 的函數(shù),為了在它們之間區(qū)別調用邦泄,我們必須使用絕對(不是相對)引用删窒。我們明確地用名稱指出 Vehicle 對象,然后在它上面調用 drive() 函數(shù)顺囊。

但如果我們說 Vehicle.drive()肌索,那么這個函數(shù)調用的 this 綁定將會是 Vehicle 對象,而不是 Car 對象(見第二章)特碳,那不是我們想要的消恍。所以,我們使用 .call( this )(見第二章)來保證 drive()Car 對象的環(huán)境中被執(zhí)行乡翅。

注意: 如果 Car.drive() 的函數(shù)名稱標識符沒有與 Vehicle.drive() 的重疊(也就是“遮蔽(shadowed)”滋迈;見第五章),我們就不會有機會演示“方法多態(tài)(method polymorphism)”益愈。因為那樣的話梢灭,一個指向 Vehicle.drive() 的引用會被 mixin(..) 調用拷貝夷家,而我們可以使用 this.drive() 直接訪問它。被選用的標識符重疊 遮蔽 就是為什么我們不得不使用更復雜的 顯式假想多態(tài)(explicit pseudo-polymorphism) 的原因敏释。

在擁有相對多態(tài)的面向類的語言中库快,CarVehicle 間的連接在類定義的頂端被建立一次,那里是維護這種關系的唯一場所钥顽。

但是由于 JavaScript 的特殊性义屏,顯式假想多態(tài)(因為遮蔽!) 在每一個你需要這種(假想)多態(tài)引用的函數(shù)中 建立了一種脆弱的手動/顯式鏈接蜂大。這可能會顯著地增加維護成本湿蛔。而且,雖然顯式假想多態(tài)可以模擬“多重繼承”的行為县爬,但這只會增加復雜性和代碼脆弱性阳啥。

這種方法的結果通常是更加復雜,更難讀懂财喳,而且 更難維護的代碼察迟。應當盡可能地避免使用顯式假想多態(tài),因為在大部分層面上它的代價要高于利益耳高。

混合拷貝(Mixing Copies)

回憶一下上面的 mixin(..) 工具:

// 大幅簡化的 `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)中沒有名稱與之匹配的屬性概荷,它就進行拷貝。因為我們是在初始對象存在的情況下進行拷貝碌燕,所以我們要小心不要將目標屬性覆蓋掉误证。

如果在指明 Car 的具體內容之前,我們先進行拷貝修壕,那么我們就可以省略對 targetObj 檢查愈捅,但是這樣做有些笨拙且低效,所以通常不優(yōu)先選用:

// 另一種 mixin慈鸠,對覆蓋不太“安全”
function mixin( sourceObj, targetObj ) {
    for (var key in sourceObj) {
        targetObj[key] = sourceObj[key];
    }

    return targetObj;
}

var Vehicle = {
    // ...
};

// 首先蓝谨,創(chuàng)建一個空對象
// 將 Vehicle 的內容拷貝進去
var Car = mixin( Vehicle, { } );

// 現(xiàn)在拷貝 Car 的具體內容
mixin( {
    wheels: 4,

    drive: function() {
        // ...
    }
}, Car );

不論哪種方法,我們都明確地將 Vehicle 中的非重疊內容拷貝到 Car 中青团∑┪祝“mixin”這個名稱來自于解釋這個任務的另一種方法:Car 混入 Vehicle 的內容,就像你吧巧克力碎片混入你最喜歡的曲奇餅面團督笆。

這個拷貝操作的結果芦昔,是 Car 將會獨立于 Vehicle 運行。如果你在 Car 上添加屬性胖腾,它不會影響到 Vehicle烟零,反之亦然瘪松。

注意: 這里有幾個小細節(jié)被忽略了咸作。仍然有一些微妙的方法使兩個對象在拷貝完成后還能互相“影響”對方锨阿,比如它們共享一個共通對象(比如數(shù)組)的引用。

由于兩個對象還共享它們的共通函數(shù)的引用记罚,這意味著 即便手動將函數(shù)從一個對象拷貝(也就是混入)到另一個對象中墅诡,也不能 實際上模擬 發(fā)生在面向類的語言中的從類到實例的真正的復制

JavaScript 函數(shù)不能真正意義上地(以標準桐智,可靠的方式)被復制末早,所以你最終得到的是同一個共享的函數(shù)對象(函數(shù)是對象;見第三章)的 被復制的引用说庭。舉例來說然磷,如果你在一個共享的函數(shù)對象(比如 ignition())上添加屬性來修改它,VehicleCar 都會通過這個共享的引用而受“影響”刊驴。

在 JavaScript 中明確的 mixin 是一種不錯的機制姿搜。但是它們顯得言過其實。和將一個屬性定義兩次相比捆憎,將屬性從一個對象拷貝到另一個對象并不會產生多少 實際的 好處舅柜。而且由于我們剛才提到的函數(shù)對象引用的微妙之處,這顯得尤為正確躲惰。

如果你明確地將兩個或更多對象混入你的目標對象致份,你可以 某種程度上模擬 “多重繼承”的行為,但是在將方法或屬性從多于一個源對象那里拷貝過來時础拨,沒有直接的辦法可以解決名稱的沖突氮块。有些開發(fā)者/庫使用“延遲綁定(late binding)”和其他詭異的替代方法來解決問題,但從根本上講诡宗,這些“技巧” 通常 得不償失(而且低效9臀)。

要小心的是僚焦,僅在明確的 mixin 能夠實際提高代碼可讀性時使用它锰提,而如果你發(fā)現(xiàn)它使代碼變得更很難追溯,或在對象間建立了不必要或笨重的依賴性時芳悲,要避免使用這種模式立肘。

如果正確使用 mixin 使你的問題變得比以前 困難,那么你可能應當停止使用 mixin名扛。事實上谅年,如果你不得不使用復雜的庫/工具來處理這些細節(jié),那么這可能標志著你正走在更困難肮韧,也許沒必要的道路上融蹂。在第六章中旺订,我們將試著提取一種更簡單的方法來實現(xiàn)我們期望的結果,同時免去這些周折超燃。

寄生繼承(Parasitic Inheritance)

明確的 mixin 模式的一個變種区拳,在某種意義上是明確的而在某種意義上是隱含的,稱為“寄生繼承(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` 是一個 `Vehicle`
    var car = new Vehicle();

    // 現(xiàn)在, 我們修改 `car` 使它特化
    car.wheels = 4;

    // 保存一個 `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!

如你所見,我們一開始從“父類”(對象)Vehicle 制造了一個定義的拷貝届良,之后將我們的“子類”(對象)定義混入其中(按照需要保留父類的引用)笆凌,最后將組合好的對象 car 作為子類實例傳遞出去。

注意: 當我們調用 new Car() 時士葫,一個新對象被創(chuàng)建并被 Carthis 所引用(見第二章)乞而。但是由于我們沒有使用這個對象,而是返回我們自己的 car 對象慢显,所以這個初始化創(chuàng)建的對象就被丟棄了爪模。因此,Car() 可以不用 new 關鍵字調用鳍怨,就可以實現(xiàn)和上面代碼相同的功能呻右,而且還可以省去對象的創(chuàng)建和回收。

隱含的 Mixin(Implicit Mixins)

隱含的 mixin 和前面解釋的 顯式假想多態(tài) 是緊密相關的鞋喇。所以它們需要注意相同的事項声滥。

考慮這段代碼:

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 (不會和 `Something` 共享狀態(tài))

Something.cool.call( this ) 既可以在“構造器”調用中使用(最常見的情況),也可以在方法調用中使用(如這里所示)侦香,我們實質上“借用”了 Something.cool() 函數(shù)并在 Another 環(huán)境下落塑,而非 Something 環(huán)境下調用它(通過 this 綁定,見第二章)罐韩。結果是憾赁,Something.cool() 中進行的賦值被實施到了 Another 對象而非 Something 對象。

那么散吵,這就是說我們將 Something 的行為“混入”了 Another龙考。

雖然這種技術看起來有效利用了 this 再綁定的功能,也就是生硬地調用 Something.cool.call( this )矾睦,但是這種調用不能被作為相對(也更靈活的)引用晦款,所以你應當 提高警惕。一般來說枚冗,應當盡量避免使用這種結構 以保持代碼干凈而且易于維護缓溅。

復習

類是一種設計模式。許多語言提供語法來啟用自然而然的面向類的軟件設計赁温。JS 也有相似的語法坛怪,但是它的行為和你在其他語言中熟悉的工作原理 有很大的不同淤齐。

類意味著拷貝。

當一個傳統(tǒng)的類被實例化時袜匿,就發(fā)生了類的行為向實例中拷貝更啄。當類被繼承時,也發(fā)生父類的行為向子類的拷貝沉帮。

多態(tài)(在繼承鏈的不同層級上擁有同名的不同函數(shù))也許看起來意味著一個從子類回到父類的相對引用鏈接锈死,但是它仍然只是拷貝行為的結果贫堰。

JavaScript 不會自動地 (像類那樣)在對象間創(chuàng)建拷貝穆壕。

mixin 模式常用于在 某種程度上 模擬類的拷貝行為,但是這通常導致像顯式假想多態(tài)那樣(OtherObj.methodName.call(this, ...))難看而且脆弱的語法其屏,這樣的語法又常導致更難懂和更難維護的代碼喇勋。

明確的 mixin 和類 拷貝 又不完全相同,因為對象(和函數(shù)Y诵小)僅僅是共享的引用被復制川背,不是對象/函數(shù)自身被復制。不注意這樣的微小之處通常是各種陷阱的根源蛤袒。

一般來講熄云,在 JS 中模擬類通常會比解決當前 真正 的問題埋下更多的坑。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末妙真,一起剝皮案震驚了整個濱河市缴允,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌珍德,老刑警劉巖练般,帶你破解...
    沈念sama閱讀 211,042評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異锈候,居然都是意外死亡薄料,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評論 2 384
  • 文/潘曉璐 我一進店門泵琳,熙熙樓的掌柜王于貴愁眉苦臉地迎上來摄职,“玉大人,你說我怎么就攤上這事获列」仁校” “怎么了?”我有些...
    開封第一講書人閱讀 156,674評論 0 345
  • 文/不壞的土叔 我叫張陵蛛倦,是天一觀的道長歌懒。 經常有香客問我,道長溯壶,這世上最難降的妖魔是什么及皂? 我笑而不...
    開封第一講書人閱讀 56,340評論 1 283
  • 正文 為了忘掉前任甫男,我火速辦了婚禮,結果婚禮上验烧,老公的妹妹穿的比我還像新娘板驳。我一直安慰自己,他們只是感情好碍拆,可當我...
    茶點故事閱讀 65,404評論 5 384
  • 文/花漫 我一把揭開白布若治。 她就那樣靜靜地躺著,像睡著了一般感混。 火紅的嫁衣襯著肌膚如雪端幼。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,749評論 1 289
  • 那天弧满,我揣著相機與錄音婆跑,去河邊找鬼。 笑死庭呜,一個胖子當著我的面吹牛滑进,可吹牛的內容都是我干的。 我是一名探鬼主播募谎,決...
    沈念sama閱讀 38,902評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼扶关,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了数冬?” 一聲冷哼從身側響起节槐,我...
    開封第一講書人閱讀 37,662評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎吉执,沒想到半個月后疯淫,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 44,110評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡戳玫,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年熙掺,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片咕宿。...
    茶點故事閱讀 38,577評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡币绩,死狀恐怖,靈堂內的尸體忽然破棺而出府阀,到底是詐尸還是另有隱情缆镣,我是刑警寧澤,帶...
    沈念sama閱讀 34,258評論 4 328
  • 正文 年R本政府宣布试浙,位于F島的核電站董瞻,受9級特大地震影響,放射性物質發(fā)生泄漏。R本人自食惡果不足惜钠糊,卻給世界環(huán)境...
    茶點故事閱讀 39,848評論 3 312
  • 文/蒙蒙 一挟秤、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧抄伍,春花似錦艘刚、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,726評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至岗喉,卻和暖如春秋度,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背沈堡。 一陣腳步聲響...
    開封第一講書人閱讀 31,952評論 1 264
  • 我被黑心中介騙來泰國打工静陈, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留燕雁,地道東北人诞丽。 一個月前我還...
    沈念sama閱讀 46,271評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像拐格,于是被迫代替她去往敵國和親僧免。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,452評論 2 348

推薦閱讀更多精彩內容