【第767期】你不懂JS:混合(淆)「類」的對象

圖片

前言前端

又到週五了,這周是否是被這一系列的文章看的有點暈,若是拿着一本書會以爲挺多的,不必定能看完,但天天跟着閱讀一篇可能就不會以爲多了,你以爲呢?今天繼續由前端早讀課專欄做者@HetfieldJoe帶來連載《你不懂JS》的分享。編程


正文從這開始~設計模式


你不懂JS:this與對象原型 第四章:混合(淆)「類」的對象數組


接着咱們上一章對對象的探索,咱們很天然的將注意力轉移到「面向對象(OO)編程」,與「類(class)」。咱們先將「面向類」做爲設計模式來看看,以後咱們再考察「類」的機制:「實例化(instantiation)」, 「繼承(inheritance)」與「相對多態(relative polymorphism)」。數據結構


咱們將會看到,這些概念並非很是天然地映射到JS的對象機制上,以及許多JavaScript開發者爲了克服這些挑戰所作的努力(mixins等)。框架


注意: 這一章花了至關一部分時間(前一半!)在着重解釋「面向對象編程」理論上。在後半部分討論「Mixins(混合)」時,咱們最終會將這些理論與真實且實際的JavaScript代碼聯繫起來。可是這裏首先要蹚過許多概念和假想代碼,因此可別迷路了——堅持下去!ide


類(Class)理論函數式編程

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


OO或者面向類的編程強調數據和操做它的行爲之間有固有的聯繫(固然,依數據的類型和性質不一樣而不一樣!),因此合理的設計是將數據和行爲打包在一塊兒(也稱爲封裝)。這有時在正式的計算機科學中稱爲「數據結構」。工具


好比,表示一個單詞或短語的一系列字符一般稱爲「string(字符串)」。這些字符就是數據。但你幾乎歷來不關心數據,你老是想對數據 作事情, 因此能夠 向 數據實施的行爲(計算它的長度,在末尾添加數據,檢索,等等)都被設計成爲String類的方法。


任何給定的字符串都是這個類的一個實例,這個類是一個整齊的集合包裝:字符數據和咱們能夠對它進行的操做功能。


類還隱含着對一個特定數據結構的一種 分類 方法。咱們這麼作的方法是,將一個給定的結構考慮爲一個更加泛化的基礎定義的具體種類。


讓咱們經過一個最常被引用的例子來探索這種分類處理。一輛 車 能夠被描述爲一「類」更泛化的東西——載具——的具體實現。


咱們在軟件中經過定義Vehicle類和Car類來模型化這種關係。


Vehicle的定義可能會包含像動力(引擎等),載人能力等等,這些都是行爲。咱們在Vehicle中定義的都是全部(或大多數)不一樣類型的載具(飛機,火車,機動車)都共同擁有的東西。


在咱們的軟件中爲每一種不一樣類型的載具一次又一次地重定義「載人能力」這個基本性質可能沒有道理。反而,咱們在Vehicle中把這個能力定義一次,以後當咱們定義Car時,咱們簡單地指出它從基本的Vehicle定義中「繼承」(或「擴展」)。Car的定義就是特化了通常的Vehicle定義。


雖然Vehicle和Car用方法的形式集約地定義了行爲,但一個實例中的數據就像一個惟一的車牌號同樣屬於一輛具體的車。


這樣,類,繼承,和實例化就誕生了。


另外一個關於類的關鍵概念是「多態(polymorphism)」,它描述這樣的想法:一個來自於父類的泛化行爲能夠被子類覆蓋,從而使它更加具體。實際上,相對多態讓咱們在覆蓋行爲中引用基礎行爲。


類理論強烈建議父類和子類對相同的行爲共享一樣的方法名,以便於子類(差別化地)覆蓋父類。咱們即將看到,在你的JavaScript代碼中這麼作會致使種種困難和脆弱的代碼。


"類(Class)"設計模式

你可能從沒把類當作一種「設計模式」考慮過,由於最多見的是關於流行的「面向對象設計模式」的討論,好比「迭代器(Iterator)」,「觀察者(Observer)」,「工廠(Factory)」,「單例(Singleton)」等等。當以這種方式表現時,幾乎能夠假定OO的類是咱們實現全部(高級)設計模式的底層機制,好像對全部代碼來講OO是一個給定的基礎。


取決於你在編程方面接受過的正規教育的水平,你可能據說過「過程式編程(procedural programming)」:一種不用任何高級抽象,僅僅由過程(也就是函數)調用其餘函數來構成的描述代碼的方式。你可能被告知過,類是一個將過程式風格的「麪條代碼」轉換爲結構良好,組織良好代碼的 恰當 的方法。


固然,若是你有「函數式編程(functional programming)」的經驗,你可能知道類只是幾種常見設計模式中的一種。可是對於其餘人來講,這多是第一次你問本身,類是否真的是代碼的根本基礎,或者它們是在代碼頂層上的選擇性抽象。


有些語言(好比Java)不給你選擇,因此這根本沒什麼 選擇性——一切都是類。其餘語言如C/C++或PHP同時給你過程式和麪向類的語法,在使用哪一種風格合適或混合風格上,留給開發者更多選擇。


JavaScript的「類」

在這個問題上JavaScript屬於哪一邊?JS擁有 一些 像類的語法元素(好比new和instanceof)有一陣子了,並且在最近的ES6中,還有一些追加的,好比class關鍵字(見附錄A)。


但這意味着JavaScript實際上 擁有 類嗎?直白且簡單:沒有。


因爲類是一種設計模式,你 能夠,用至關的努力(咱們將在本章剩下的部分看到),近似實現不少經典類的功能。JS在經過提供看起來像類的語法,來努力知足用類進行設計的極其普遍的渴望。


雖然咱們好像有了看起來像類的語法,可是好像JavaScript機制在抵抗你使用 類設計模式,由於在底層,這些你正在上面工做的機制運行的十分不一樣。語法糖和(極其普遍被使用的)JS「Class」庫廢了很大力氣來把這些真實狀況對你隱藏起來,但你早晚會面對現實:你在其餘語言中遇到的 類 和你在JS中模擬的「類」不一樣。


總而言之,類是軟件設計中的一種可選模式,你能夠選擇在JavaScript中使用或不使用它。由於許多開發者都對面向類的軟件設計情有獨鍾,咱們將在本章剩下的部分中探索一下,爲了使用JS提供的東西維護類的幻覺要付出什麼代價,和咱們經歷的痛苦。


Class 機制

在許多面向類語言中,「標準庫」都提供一個叫「棧」(壓棧,彈出等)的數據結構,用一個Stack類表示。這個類擁有一組變量來存儲數據,還擁有一組可公開訪問的行爲(「方法」),這些行爲使你的代碼有能力與(隱藏的)數據互動(添加或移除數據等等)。


可是在這樣的語言中,你不是直接在Stack上操做(除非製造一個 靜態的 類成員引用,但這超出了咱們要討論的範圍)。Stack類僅僅是 任何 的「棧」都會作的事情的一個抽象解釋,但它自己不是一個「棧」。爲了獲得一個能夠對之進行操做的實在的數據結構,你必須 實例化 這個Stack類。


建築物

傳統的"類(class)"和"實例(instance)"的比擬源自於建築物的建造。


一個建築師會規劃出一棟建築的全部性質:多寬,多高,在哪裏有多少窗戶,甚至牆壁和天花板用什麼材料。在這個時候,她並不關心建築物將會被建造在 哪裏,她也不關心有 多少 這棟建築的拷貝將被建造。


同時她也不關心這棟建築的內容——傢俱,牆紙,吊扇等等——她僅關心建築物含有何種結構。


她生產的建築學上的藍圖僅僅是建築物的「方案」。它們不實際構成咱們能夠實在進入其中並坐下的建築物。爲了這個任務咱們須要一個建築工。建築工會拿走方案並精確地依照它們 建造 這棟建築物。在真正的意義上,他是在將方案中意圖的性質 拷貝 到物理建築物中。


一旦完成,這棟建築就是藍圖方案的一個物理實例,一個有望是實質完美的 拷貝。而後建築工就能夠移動到隔壁將它再重作一遍,建造另外一個 拷貝。


建築物與藍圖間的關係是間接的。你能夠檢視藍圖來了解建築物是如何構造的,但對於直接考察建築物的每一部分,僅有藍圖是不夠的。若是你想打開一扇門,你不得不走進建築物自身——藍圖僅僅是爲了用來 表示 門的位置而在紙上畫的線條。


一個類就是一個藍圖。爲了實際獲得一個對象並與之互動,咱們必須從類中建造(也就是實例化)某些東西。這種「構建」的最終結果是一個對象,典型地稱爲一個「實例」,咱們能夠按須要直接調用它的方法,訪問它的公共數據屬性。


這個對象是全部在類中被描述的特性的 拷貝。


你不太期望走進一棟建築以後發現,一份用於規劃這棟建築物的藍圖被裱起來掛在牆上,雖然藍圖可能在辦公室的公共記錄的文件中。類似地,你通常不會使用對象實例來直接訪問和操做類,可是這至少對於斷定對象實例來自於 哪一個類 是可能的。


與考慮對象實例與它源自的類的任何間接關係相比,考慮類和對象實例的直接關係更有用。一個類經過拷貝操做被實例化爲對象的形式。

圖片


如你所見,箭頭由左向右,從上至下,這表示着概念上和物理上發生的拷貝操做。


構造器(Constructor)

類的實例由類的一種特殊方法構建,這個方法的名稱一般與類名相同,稱爲 「構造器(constructor)」。這個方法的明確的工做,就是初始化實例所需的全部信息(狀態)。


好比,考慮下面這個類的假想代碼(語法是自創的):

圖片

爲了 製造 一個CoolGuy實例,咱們須要調用類的構造器:

圖片

注意,CoolGuy類有一個構造器CoolGuy(),它實際上就是在咱們說new CoolGuy(..)時調用的。咱們從這個構造器拿回一個對象(類的一個實例),咱們能夠調用showOff()方法,來打印這個特定的CoolGuy的特殊才藝。


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


類的構造器 屬於 那個類,幾乎老是和類同名。同時,構造器大多數狀況下老是須要用new來調用,以便使語言的引擎知道你想要構建一個 新的 類的實例。


類繼承(Class Inheritance)

在面向類的語言中,你不只能夠定義一個能夠初始化它本身的類,你還能夠定義另一個類 繼承 自第一個類。


這第二個類一般被稱爲「子類」,而第一個類被稱爲「父類」。這些名詞明顯地來自於親子關係的比擬,雖然這種比擬有些扭曲,就像你立刻要看到的。


當一個家長擁有一個和他有血緣關係的孩子時,家長的遺傳性質會被拷貝到孩子身上。明顯地,在大多數生物繁殖系統中,雙親都平等地貢獻基因進行混合。可是爲了這個比擬的目的,咱們假設只有一個親人。


一旦孩子出現,他或她就從親人那裏分離出來。這個孩子受其親人的繼承因素的嚴重影響,可是獨一無二。若是這個孩子擁有紅色的頭髮,這並不意味這他的親人的頭髮 曾經 是紅色,或者會自動 變成 紅色。


以類似的方式,一旦一個子類被定義,它就分離且區別於父類。子類含有一份從父類那裏得來的行爲的初始拷貝,但它能夠覆蓋這些繼承的行爲,甚至是定義新行爲。


重要的是,要記住咱們在討論父 類 和子 類,而不是物理上的東西。這就是這個親子比擬讓人糊塗的地方,由於咱們實際上應當說父類就是親人的DNA,而子類就是孩子的DNA。咱們不得不從兩套DNA製造出(也就是初始化)人,用獲得的物理上存在的人來與之進行談話。


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


讓咱們從新審視本章前面的Vehicle和Car的討論。考慮下面表達繼承的類的假想代碼:

image.png


注意: 爲了簡潔明瞭,這些類的構造器被省略了。


咱們定義Vehicle類,假定它有一個引擎,有一個打開打火器的方法,和一個行駛的方法。但你永遠也不會製造一個泛化的「載具」,因此在這裏它只是一個概念的抽象。


而後咱們定義了兩種具體的載具:Car和SpeedBoat。它們都繼承Vehicle的泛化性質,但以後它們都對這些性質進行了合適的特化。一輛車有4個輪子,一艘快艇有兩個引擎,意味着它須要在打火時須要特別注意要啓動兩個引擎。


多態(Polymorphism)

Car定義了本身的drive()方法,它覆蓋了從Vehicle繼承來的同名方法。可是,Car的drive()方法調用了inherited:drive(),這表示Car能夠引用它繼承的,覆蓋以前的原版drive()。SpeedBoat的pilot()方法也引用了它繼承的drive()拷貝。


這種技術稱爲「多態(polymorphism)」,或「虛擬多態(virtual polymorphism)」。對咱們當前的狀況更具體一些,咱們稱之爲「相對多態(relative polymorphism)」。


多態這個話題比咱們能夠在這裏談到的內容要寬泛的多,但咱們當前的「相對」意味着一個特殊層面:任何方法均可以引用位於繼承層級上更高一層的其餘方法(同名或不一樣名)。咱們說「相對」,由於咱們不絕對定義咱們想訪問繼承的哪一層(也就是類),而實質上在說「向上一層」來相對地引用。


在許多語言中,在這個例子中使用inherited:的地方使用了super關鍵字,它的基於這樣的想法:一個「超類(super class)」是當前類的父親/祖先。


多態的另外一個方面是,一個方法名能夠在繼承鏈的不一樣層面上有多種定義,並且在解析哪一個方法在被調用時,這些定義能夠適當地被自動選擇。


在咱們上面的例子中,咱們看到這種行爲發生了兩次:drive()在Vehicle和Car中定義, 而ignition()在Vehicle和SpeedBoat中定義。


注意: 另外一個傳統面向類語言經過super給你的能力,是從子類的構造器中直接訪問父類構造器。這很大程度上是對的,由於對真正的類來講,構造器屬於這個類。然而在JS中,這是相反的——實際上認爲「類」屬於構造器(Foo.prototype...類型引用)更恰當。由於在JS中,父子關係僅存在於它們各自的構造器的兩個.prototype對象間,構造器自己不直接關聯,並且沒有簡單的方法從一箇中相對引用另外一個(參見附錄A,看看ES6中用super「解決」此問題的class)。


能夠從ignition()中具體看出多態的一個有趣的含義。在pilot()內部,一個相對多態引用指向了(繼承的)Vehicle版本的drive()。而這個drive()僅僅經過名稱(不是相對引用)來引用ignition()方法。


語言的引擎會使用哪個版本的ignition()?是Vehicle的仍是SpeedBoat的?它會使用SpeedBoat版本的ignition()。 若是你 能 初始化Vehicle類自身,而且調用它的drive(),那麼語言引擎將會使用Vehicle的ignition()定義。


換句話說,ignition()方法的定義,根據你引用的實例是哪一個類(繼承層級)而 多態(改變)。


這看起來過於深刻學術細節了。不過爲了好好地與JavaScript的[[Prototype]]機制的相似行爲進行對比,理解這些細節仍是很重要的。


若是類是繼承而來的,對這些類自己(不是由它們建立的對象)有一個方法能夠 相對地 引用它們繼承的對象,這個相對引用一般稱爲super。


記得剛纔這幅圖:

image.png


注意對於實例化(a1,a2,b1,和b2) 和 繼承(Bar),箭頭如何表示拷貝操做。


從概念上講,看起來子類Bar可使用相對多態引用(也就是super)來訪問它的父類Foo的行爲。然而在現實中,子類不過是被給與了一份它從父類繼承來的行爲的拷貝而已。若是子類「覆蓋」一個它繼承的方法,原版的方法和覆蓋版的方法實際上都是存在的,因此它們都是能夠訪問的。


不要讓多態把你搞糊塗,使你認爲子類是連接到父類上的。子類獲得一份它須要從父類繼承的東西的拷貝。類繼承意味着拷貝。


多重繼承(Multiple Inheritance)

能回想起咱們早先提到的親子和DNA嗎?咱們說過這個比擬有些奇怪,由於生物學上大多數後代來自於雙親。若是類能夠繼承自其餘兩個類,那麼這個親子比擬會更合適一些。


有些面向類的語言容許你指定一個以上的「父類」來進行「繼承」。多重繼承意味着每一個父類的定義都被拷貝到子類中。


表面上看來,這是對面向類的一個強大的加成能力,給咱們能力去將更多功能組合在一塊兒。然而,這無疑會產生一些複雜的問題。若是兩個父類都提供了名爲drive()的方法,在子類中的drive()引用將會解析爲哪一個版本?你會老是不得不手動指明哪一個父類的drive()是你想要的,從而失去一些多態繼承的優雅之處嗎?


還有另一個所謂的「鑽石問題」:子類「D」繼承自兩個父類(「B」和「C」),它們兩個又繼承自共通的父類「A」。若是「A」提供了方法drive(),而「B」和「C」都覆蓋(多態地)了這個方法,那麼當「D」引用drive()時,它應當使用那個版本呢(B:drive()仍是C:drive())?

image.png


事情會比咱們這樣窺豹一斑能看到的複雜得多。咱們在這裏把它們記下來,以便於咱們能夠將它和JavaScript機制的工做方式比較。


JavaScript更簡單:它不爲「多重繼承」提供原生機制。許多人認爲這是好事,由於省去的複雜性要比「減小」的功能多得多。可是這並不能阻擋開發者們用各類方法來模擬它,咱們接下來就看看。


Mixins(混合)

當你「繼承」或是「實例化」時,JavaScript的對象機制不會 自動地 執行拷貝行爲。很簡單,在JavaScript中沒有「類」能夠拿來實例化,只有對象。並且對象也不會被拷貝到另外一個對象中,而是被 連接在一塊兒(詳見第五章)。


由於在其餘語言中觀察到的類的行爲意味着拷貝,讓咱們來看看JS開發者如何在JavaScript中 模擬 這種 缺失 的類的拷貝行爲:mixins(混合)。咱們會看到兩種「mixin」:明確的(explicit) 和 隱含的(implicit)。


明確的 Mixins(Explicit Mixins)

讓咱們再次回顧前面的Vehicle和Car的例子。由於JavaScript不會自動地將行爲從Vehicle拷貝到Car,咱們能夠建造一個工具來手動拷貝。這樣的工具常常被許多包/框架稱爲extend(..),但爲了說明的目的,咱們在這裏叫它mixin(..)。

image.png


注意: 重要的細節:咱們談論的再也不是類,由於在JavaScript中沒有類。Vehicle和Car分別只是咱們實施拷貝的源和目標對象。


Car如今擁有了一份從Vehicle獲得的屬性和函數的拷貝。技術上講,函數實際上沒有被複制,而是指向函數的 引用 被複制了。因此,Car如今有一個稱爲ignition的屬性,它是一個ignition()函數引用的拷貝;並且它還有一個稱爲engines的屬性,持有從Vehicle拷貝來的值1。


Car已經 有了drive屬性(函數),因此這個屬性引用沒有被覆蓋(參見上面mixin(..)的if語句)。


重溫"多態(Polymorphism)"

咱們來考察一下這個語句:Vehicle.drive.call( this )。我將之稱爲「顯式假想多態(explicit pseudo-polymorphism)」。回想咱們前一段假想代碼的這一行是咱們稱之爲「相對多態(relative polymorphism)」的inherited:drive()。


JavaScript沒有能力實現相對多態(ES6以前,見附錄A)。因此,由於Car和Vehicle都有一個名爲drive()的函數,爲了在它們之間區別調用,咱們必須使用絕對(不是相對)引用。咱們明確地用名稱指出Vehicle對象,而後在它上面調用drive()函數。


但若是咱們說Vehicle.drive(),那麼這個函數調用的this綁定將會是Vehicle對象,而不是Car對象(見第二章),那不是咱們想要的。因此,咱們使用.call( this )(見第二章)來保證drive()在Car對象的環境中被執行。


注意: 若是Car.drive()的函數名稱標識符沒有與Vehicle.drive()的重疊(也就是「遮蔽」;見第五章),咱們就不會有機會演示「方法多態(method polymorphism)」。由於那樣的話,一個指向Vehicle.drive()的引用會被mixin(..)調用拷貝,而咱們可使用this.drive()直接訪問它。被選用的標識符重疊 遮蔽 就是爲何咱們不得不使用更復雜的 顯式假想多態(explicit pseudo-polymorphism) 的緣由。


在擁有相對多態的面向類的語言中,Car和Vehicle間的鏈接被創建一次,就在類定義的頂端,這裏是維護這種關係的惟一場所。


可是因爲JavaScript的特殊性,顯式假想多態(由於遮蔽!) 在每個你須要這種(假想)多態引用的函數中 創建了一種脆弱的手動/顯式連接。這可能會顯著地增長維護成本。並且,雖然顯式假想多態能夠模擬「多重繼承」的行爲,但這隻會增長複雜性和代碼脆弱性。


這種方法的結果一般是更加複雜,更難讀懂,並且 更難維護的代碼。應當儘量地避免使用顯式假想多態,由於在大部分層面上它的代價要高於利益。


混合拷貝(Mixing Copies)

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

image.png


如今,咱們考察一下mixin(..)如何工做。它迭代sourceObj(在咱們的例子中是Vehicle)的全部屬性,若是在targetObj (在咱們的例子中是Car)中沒有名稱與之匹配的屬性,它就進行拷貝。由於咱們是在初始對象存在的狀況下進行拷貝,因此咱們要當心不要將目標屬性覆蓋掉。


若是在指明Car的具體內容以前,咱們先進行拷貝,那麼咱們就能夠省略對targetObj檢查,可是這樣作有些笨拙且低效,因此一般不優先選用:

image.png


不論哪一種方法,咱們都顯式地將Vehicle中的非重疊內容拷貝到Car中。「mixin」這個名稱來自於解釋這個任務的另外一種方法:Car混入Vehicle的內容,就像你吧巧克力碎片混入你最喜歡的曲奇餅麪糰。


這個拷貝操做的結果,是Car將會獨立於Vehicle運行。若是你在Car上添加屬性,它不會影響到Vehicle,反之亦然。


注意: 這裏有幾個小細節被忽略了。仍然有一些微妙的方法使兩個對象在拷貝完成後還能互相「影響」對方,好比他們共享一個共通對象(好比數組)的引用。


因爲兩個對象還共享它們的共通函數的引用,這意味着 即使手動將函數從一個對象拷貝(也就是混入)到另外一個對象中,也不能 實際上模擬 發生在面向類的語言中的從類到實例的真正的複製。


JavaScript函數不能真正意義上地被複制(以標準,可靠的方式),因此你最終獲得的是同一個共享的函數對象(函數是對象;見第三章)的 被複制的引用。舉例來講,若是你在一個共享的函數對象(好比ignition())上添加屬性來修改它,Vehicle和Car都會經過這個共享的引用而受影響。


在JavaScript中明確的mixin是一種不錯的機制。可是它們顯得言過其實。和將一個屬性定義兩次相比,將屬性從一個對象拷貝到另外一個對象並不會產生多少 實際的 好處。這對咱們剛纔提到的給函數對象引用增長的微妙變化來講,顯得尤其正確。


若是你明確地將兩個或更多對象混入你的目標對象,你能夠 某種程度上模擬 「多重繼承」的行爲,可是在將方法或屬性從多於一個源對象那裏拷貝過來時,沒有直接的辦法能夠解決名稱的衝突。有些開發者/包使用「late binding(延遲綁定)」和其餘詭異的替代方法來解決問題,但從根本上講,這些「技巧」 一般 得不償失(並且低效!)。


要當心的是,僅在明確的mixin可以實際提升代碼可讀性時使用它,而若是你發現它使代碼變得更很難追溯,或在對象間創建了沒必要要或笨重的依賴性時,要避免使用這種模式。


若是正確使用mixin使你的問題變得比之前 困難,那麼你可能應當中止使用mixin。實際上,若是你不得不使用複雜的包/工具來處理這些細節,這可能標誌着你正走在更困難,也許不必的道路上。在第六章中,咱們將試着提取一種更簡單的方法來實現咱們指望的結果,同時免去這些周折。


寄生繼承(Parasitic Inheritance)

明確的mixin模式的一個變種,在某種意義上是明確的而在某種意義上是隱含的,稱爲「寄生繼承(Parasitic Inheritance)」,它主要是由Douglas Crockford推廣的。


這是它如何工做:

image.png


如你所見,咱們一開始從「父類」(對象)Vehicle製造了一個定義的拷貝,以後將咱們的「子類」(對象)定義混入其中(按照須要保留父類的引用),最後將組合好的對象car做爲子類實例傳遞出去。


注意: 當咱們調用new Car()時,一個新對象被建立並被Car的this所引用(見第二章)。可是因爲咱們沒有使用這個對象,而是返回咱們本身的car對象,因此這個初始化建立的對象就被丟棄了。因此,Car()能夠不用new關鍵字調用,就能夠實現和上面代碼相同的功能,並且還能夠節省對象的建立和回收。


隱含的 Mixin(Implicit Mixins)

隱含的mixin和前面解釋的 顯式假想多態 是緊密相關的。因此它們須要注意相同的事項。


考慮這段代碼:

圖片


Something.cool.call( this )既能夠在「構造器」調用中使用(最多見的狀況),也能夠在方法調用中使用(如這裏所示),咱們實質上「借用」了Something.cool()函數並在Another環境下,而非Something環境下調用它(經過this綁定,見第二章)。結果是,Something.cool()中進行的賦值被實施到了Another對象而非Something對象。


那麼,這就是說咱們將Something的行爲「混入」了Another。


雖然這種技術看起來有效利用了this再綁定的功能,也就是生硬地調用Something.cool.call( this ),可是這種調用不能被做爲相對(也更靈活的)引用,因此你應當 提升警戒。通常來講,儘可能避免使用這種結構 來保持代碼乾淨並且容易維護。


複習

類是一種設計模式。許多語言提供語法來啓用天然而然的面向類的軟件設計。JS也有類似的語法,可是它的行爲和你在其餘語言中熟悉的工做原理 有很大的不一樣。


類意味着拷貝。


當一個傳統的類被實例化時,就發生了類的行爲向實例中拷貝。當類被繼承時,也發生父類的行爲向子類的拷貝。


多態(在繼承鏈的不一樣層級上擁有同名的不一樣函數)也許看起來意味着一個從子類回到父類的相對引用連接,可是它仍然只是拷貝行的的結果。


JavaScript 不會自動地 (像類那樣)在對象間建立拷貝。


mixin模式經常使用於在 某種程度上 模擬類的拷貝行爲,可是這一般致使像顯式假想多態那樣(OtherObj.methodName.call(this, ...))難看並且脆弱的語法,這樣的語法又常致使更難懂和更難維護的代碼。


顯式mixin和類 拷貝 又不徹底相同,由於對象(和函數!)僅僅是共享的引用被複制,不是對象/函數自身被複制。不注意這樣的微小之處一般是各類陷阱的根源。


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

相關文章
相關標籤/搜索