前言前端
這一系列的分享,好多夥伴都說文章太長了。平時都講究快閱讀,有時候能夠試試慢閱讀,何不挑戰下本身呢?今天分享的是this與對象原型的倒數第二章。今天繼續由前端早讀課專欄做者@HetfieldJoe帶來連載《你不懂JS》的分享。編程
正文從這開始~設計模式
你不懂JS:this與對象原型 第六章:行爲委託瀏覽器
在【第772期】你不懂JS:原型(Prototype)中,咱們詳細地討論了[[Prototype]]機制,和 爲何 對於描述「類」或「繼承」來講它是那麼令人糊塗和不合適。咱們一路跋涉,不只涉及了至關繁冗的語法(使代碼凌亂的.prototype),還有各類陷阱(好比令人吃驚的.constructor解析和難看的假想多態語法)。咱們探索了許多人試圖用抹平這些粗糙的區域而使用的各類「mixin」方法。服務器
這時一個常見的反應是,想知道爲何這些看起來如此簡單的事情這麼複雜。如今咱們已經拉開帷幕看到了它是多麼麻煩,這並不奇怪:大多數JS開發者從不探究得這麼深,而將這一團糟交給一個「類」包去幫他們處理。閉包
我但願到如今你不會甘心於敷衍了事並把這樣的細節丟給一個「黑盒」庫。如今咱們來深刻講解咱們 如何與應當如何 以一種比類形成的困惑 簡單得多並且更直接的方式 來考慮JS中對象的[[Prototype]]機制。架構
簡單地複習一下第五章的結論,[[Prototype]]機制是一種存在於一個對象上的內部連接,它指向一個其餘對象。app
當一個屬性/方法引用在第一個對象上發生,而這樣的屬性/方法又不存在時,這個連接就會被使用。在這種狀況下,[[Prototype]]連接告訴引擎去那個被連接的對象上尋找該屬性/方法。接下來,若是那個對象也不能知足查詢,就沿着它的[[Prototype]]查詢,如此繼續。這種對象間一系列的連接構成了所謂的「原形鏈」。框架
換句話說,對於咱們能在JavaScript中利用的功能的實際機制來講,其重要的實質 所有在於被鏈接到其餘對象的對象。ide
這個觀點是理解本章其他部分的動機和方法的重要基礎!
邁向面向委託的設計
爲了將咱們的思想恰當地集中在如何用最直截了當的方法使用[[Prototype]],咱們必須認識到它表明一種根本上與類不一樣的設計模式(【第767期】你不懂JS:混合(淆)「類」的對象)。
注意* 某些 面相類的設計依然是頗有效的,因此不要扔掉你知道的每一件事(扔掉大多數就好了!)。好比,封裝 就十分強大,並且與委託兼容的(雖然不那麼常見)。
咱們須要試着將咱們的思惟從類/繼承的設計模式轉變爲行爲代理設計模式。若是你已經用在教育/工做生涯中思考類的方式作了大多數或全部的編程工做,這可能感受不舒服或不天然。你可能須要嘗試這種思惟過程好幾回,才能適應這種很是不一樣的思考方式。
我將首先帶你進行一些理論練習,以後咱們會一對一地看一些更實際的例子來爲你本身的代碼提供實踐環境。
類理論
比方說咱們有幾個類似的任務(「XYZ」,「ABC」,等)須要在咱們的軟件中建模。
使用類,你設計這個場景的方式是:定義一個泛化的父類(基類)好比Task,爲全部的「同類」任務定義共享的行爲。而後,你定義子類XYZ和ABC,它們都繼承自Task,每一個都分別添加了特化的行爲來處理各自的任務。
重要的是, 類設計模式將鼓勵你發揮繼承的最大功效,當你在XYZ任務中覆蓋Task的某些泛化方法的定義時,你將會想利用方法覆蓋(和多態),也許會利用super來調用這個方法泛化版本,爲它添加更多的行爲。你極可能會找到幾個能夠「抽象」到父類中,或在子類中特化(覆蓋)的地方。
這是一些關於這個場景的假想代碼:
如今,你能夠初始化一個或多個XYZ子類的 拷貝,而且使用這些實例來執行「XYZ」任務。這些實例已經 同時拷貝 了泛化的Task定義的行爲和具體的XYZ定義的行爲。相似地,ABC類的實例將拷貝Task的行爲和具體的ABC的行爲。在構建完成以後,你通常會僅與這些實例互動(而不是類),由於每一個實例都拷貝了完成計劃任務的全部行爲。
委託理論
可是如今然咱們試着用 行爲委託 代替 類 來思考一樣的問題。
你將首先定義一個稱爲Task的 對象(不是一個類,也不是一個大多數JS開發者想讓你相信的function),並且它將擁有具體的行爲,這些行爲包含各類任務可使用的(讀做:委託至!)工具方法。而後,對於每一個任務(「XYZ」,「ABC」),你定義一個 對象 來持有這個特定任務的數據/行爲。你 連接 你的特定任務對象到Task工具對象,容許它們在必要的時候能夠委託到它。
基本上,你認爲執行任務「XYZ」就是從兩個兄弟/對等的對象(XYZ和Task)中請求行爲來完成它。與其經過類的拷貝將它們組合在一塊兒,咱們能夠將他們保持在分離的對象中,並且能夠在須要的狀況下容許XYZ對象來 委託到 Task。
這裏是一些簡單的代碼,示意你如何實現它:
在這段代碼中,Task和XYZ不是類(也不是函數),它們 僅僅是對象。XYZ經過Object.create()建立,來[[Prototype]]委託到Task對象(見第五章)。
做爲與面相類(也就是,OO——面相對象)的對比,我稱這種風格的代碼爲 「OLOO」(objects-linked-to-other-objects(連接到其餘對象的對象))。全部咱們 真正 關心的是,對象XYZ委託到對象Task(對象ABC也同樣)。
在JavaScript中,[[Prototype]]機制將 對象 連接到其餘 對象。不管你多麼想說服本身這不是真的,JavaScript沒有像「類」那樣的抽象機制。這就像逆水行舟:你 能夠 作到,但你 選擇 了逆流而上,因此很明顯地,你會更困難地達到目的地。
OLOO風格的代碼 中有一些須要注意的不一樣:
前一個類的例子中的id和label數據成員都是XYZ上的直接數據屬性(它們都不在Task上)。通常來講,當[[Prototype]]委託引入時,你想使狀態保持在委託者上(XYZ,ABC),不是在委託上(Task)。
在類的設計模式中,咱們故意在父類(Task)和子類(XYZ)上採用相同的命名outputTask,以致於咱們能夠利用覆蓋(多態)。在委託的行爲中,咱們反其道而行之:咱們盡一切可能避免在[[Prototype]]鏈的不一樣層級上給出相同的命名(稱爲「遮蔽」——見第五章),由於這些命名衝突會致使尷尬/脆弱的語法來消除引用的歧義(見第四章),而咱們想避免它。
這種設計模式不那麼要求那些傾向於被覆蓋的泛化的方法名,而是要求針對於每一個對象的 具體 行爲類型給出更具描述性的方法名。這實際上會產生更易於理解/維護的代碼,由於方法名(不只在定義的位置,而是擴散到其餘代碼中)變得更加明白(代碼即文檔)。
this.setID(ID);位於對象XYZ的一個方法內部,它首先在XYZ上查找setID(..),但由於它不能在XYZ上找到叫這個名稱的方法,[[Prototype]]委託意味着它能夠沿着連接到Task來尋找setID(),這樣固然就找到了。另外,因爲調用點的隱含this綁定規則(見第二章),當setID()運行時,即使方法是在Task上找到的,這個函數調用的this綁定依然是咱們指望和想要的XYZ。咱們在代碼稍後的this.outputID()中也看到了一樣的事情。
換句話說,咱們可使用存在於Task上的泛化工具與XYZ互動,由於XYZ能夠委託至Task。
行爲委託 意味着:在某個對象(XYZ)的屬性或方法沒能在這個對象(XYZ)上找到時,讓這個對象(XYZ)爲屬性或方法引用提供一個委託(Task)。
這是一個 極其強大 的設計模式,與父類和子類,繼承,多態等有很大的不一樣。與其在你的思惟中縱向地,從上面父類到下面子類地組織對象,你應帶並列地,對等地考慮對象,並且對象間擁有方向性的委託連接。
注意: 委託更適於做爲內部實現的細節,而不是直接暴露在API接口的設計中。在上面的例子中,咱們的API設計不必有意地讓開發者調用XYZ.setID()(固然咱們能夠!)。咱們以某種隱藏的方式將委託做爲咱們API的內部細節,即XYZ.prepareTask(..)委託到Task.setID(..)。詳細的內容,參照第五章的「連接做爲候補?」中的討論。
相互委託(不容許)
你不能在兩個或多個對象間相互地委託(雙向地)對方來建立一個 循環 。若是你使B連接到A,而後試着讓A連接到B,那麼你將獲得一個錯誤。
這樣的事情不被容許有些惋惜(不是很是使人驚訝,但稍稍有些惱人)。若是你製造一個在任意一方都不存在的屬性/方法引用,你就會在[[Prototype]]上獲得一個無限遞歸的循環。但若是全部的引用都嚴格存在,那麼B就能夠委託至A,或相反,並且它能夠工做。這意味着你能夠爲了多種任務用這兩個對象互相委託至對方。有一些狀況這可能會有用。
但它不被容許是由於引擎的實現者發現,在設置時檢查(並拒絕!)無限循環引用一次,要比每次你在一個對象上查詢屬性時都作相同檢查的性能要高。
調試
咱們將簡單地討論一個可能困擾開發者的微妙的細節。通常來講,JS語言規範不會控制瀏覽器開發者工具如何向開發者表示指定的值/結構,因此每種瀏覽器/引擎都自由地按須要解釋這個事情。所以,瀏覽器/工具 不老是意見統一。特別地,咱們如今要考察的行爲就是當前僅在Chrome的開發者工具中觀察到的。
考慮這段傳統的「類構造器」風格的JS代碼,正如它將在Chrome開發者工具 控制檯 中出現的:
讓咱們看一下這個代碼段的最後一行:對錶達式a1進行求值的輸出,打印Foo {}。若是你在FireFox中試用一樣的代碼,你極可能會看到Object {}。爲何會有不一樣?這些輸出意味着什麼?
Chrome實質上在說「{}是一個由名爲‘Foo’的函數建立的空對象」。Firefox在說「{}是一個由Object普通構建的空對象」。這種微妙的區別是由於Chrome在像一個 內部屬性 同樣,動態跟蹤執行建立的實際方法的名稱,而其餘瀏覽器不會跟蹤這樣的附加信息。
試圖用JavaScript機制來解釋它很吸引人:
那麼,Chrome就是經過簡單地查看對象的.Constructor.name來輸出「Foo」的?使人費解的是,答案既是「是」也是「不」。
考慮下面的代碼:
即使咱們將a1.constructor.name合法地改變爲其餘的東西(「Gotcha」),Chrome控制檯依舊使用名稱「Foo」。
那麼,說明前面問題(它使用.constructor.name嗎?)的答案是 不,他必定在內部追蹤其餘的什麼東西。
可是,且慢!讓咱們看看這種行爲如何與OLOO風格的代碼一塊兒工做:
啊哈!Gotcha,Chrome的控制檯 確實 尋找而且使用了.constructor.name。實際上,就在寫這本書的時候,正是這個行爲被認定爲是Chrome的一個Bug,並且就在你讀到這裏的時候,它可能已經被修復了。因此你可能已經看到了被修改過的 a1; // Object{}。
這個bug暫且不論,Chrome執行的(剛剛在代碼段中展現的)「構造器名稱」內部追蹤(目前僅用於調試輸出的目的),是一個僅在Chrome內部存在的擴張行爲,它已經超出了JS語言規範要求的範圍。
若是你不使用「構造器」來製造你的對象,就像咱們在本章的OLOO風格代碼中不鼓勵的那樣,那麼你將會獲得一個Chrome不會爲其追蹤內部「構造器名稱」的對象,因此這樣的對象將正確地僅僅被輸出「Object {}」,意味着「從Object()構建生成的對象」。
不要認爲 這表明一個OLOO風格代碼的缺點。當你用OLOO編碼並且用行爲代理做爲你的設計模式時,誰 「建立了」(也就是,哪一個函數 被和new一塊兒調用了?)一些對象是一個無關的細節。Chrome特殊的內部「構造器名稱」追蹤僅僅在你徹底接受「類風格」編碼時纔有用,而在你接受OLOO委託時是沒有意義的。
思惟模型比較
如今你至少在理論上能夠看到「類」和「委託」設計模式的不一樣了,讓咱們看看這些設計模式在咱們用來推導咱們代碼的思惟模型上的含義。
咱們將查看一些更加理論上的(「Foo」,「Bar」)代碼,而後比較兩種方法(OO vs. OLOO)的代碼實現。第一段代碼使用經典的(「原型的」)OO風格:
父類Foo,被子類Bar繼承,以後Bar被初始化兩次:b1和b2。咱們獲得的是b1委託至Bar.prototype,Bar.prototype委託至Foo.prototype。這對你來講應當看起來十分熟悉。沒有太具開拓性的東西發生。
如今,讓咱們使用 OLOO 風格的代碼 實現徹底相同的功能:
咱們利用了徹底相同的從Bar到Foo的[[Prototype]]委託,正如咱們在前一個代碼段中b1,Bar.prototype,和Foo.prototype之間那樣。咱們仍然有3個對象連接在一塊兒。
但重要的是,咱們極大地簡化了發生的 全部其餘事項,由於咱們如今僅僅創建了相互連接的 對象,而不須要全部其餘討厭且困惑的看起來像類(但動起來不像)的東西,還有構造器,原型和new調用。
問問你本身:若是我能用OLOO風格代碼獲得我用「類」風格代碼獲得的同樣的東西,但OLOO更簡單並且須要考慮的事情更少,OLOO不是更好嗎?
讓咱們講解一下這兩個代碼段間涉及的思惟模型。
首先,類風給的代碼段意味着這樣的實體與它們的關係的思惟模型:
實際上,這有點兒不公平/誤導,由於它展現了許多額外的,你在 技術上 一直不須要知道(雖然你 須要 理解它)的細節。一個關鍵是,它是一系列十分複雜的關係。但另外一個關鍵是:若是你花時間來沿着這些關係的箭頭走,在JS的機制中 有數量驚人的內部統一性。
例如,JS函數能夠訪問call(..),apply(..)和bind(..)(見第二章)的能力是由於函數自己是對象,而函數對象還擁有一個[[Prototype]]連接,鏈到Function.prototype對象,它定義了那些任何函數對象均可以委託到的默認方法。JS能夠作這些事情,你也能!
好了,如今讓咱們看一個這張圖的 稍稍 簡化的版本,用它來進行比較稍微「公平」一點——它僅展現了 相關 的實體與關係。
任然很是複雜,對吧?虛線描繪了當你在Foo.prototype和Bar.prototype間創建「繼承」時的隱含關係,並且尚未 修復 丟失的 .constructor屬性引用(見第五章「終極構造器」)。即使將虛線去掉,每次你與對象連接打交道時,這個思惟模型依然要變不少可怕的戲法。
如今,然咱們看看OLOO風格代碼的思惟模型:
正如你所比較它們獲得的,十分明顯,OLOO風格的代碼 須要關心的東西少太多了,由於OLOO風格代碼接受了 事實:咱們惟一須要真正關心的事情是 連接到其餘對象的對象。
全部其餘「類」的爛設計用一種使人費解並且複雜的方式獲得相同的結果。去掉那些東西,事情就變得簡單得多(還不會失去任何功能)。
Classes vs. Objects
咱們已經看到了各類理論的探索和「類」與「行爲委託」的思惟模型的比較。如今讓咱們來看看更具體的代碼場景,來展現你如何實際應用這些想法。
咱們將首先講解一種在前端網頁開發中的典型場景:建造UI部件(按鈕,下拉列表等等)。
Widget「類」
由於你可能仍是如此地習慣於OO設計模式,你極可能會當即這樣考慮這個問題:一個父類(也許稱爲Wedget)擁有全部共通的基本部件行爲,而後衍生的子類擁有具體的部件類型(好比Button)。
注意: 爲了DOM和CSS的操做,咱們將在這裏使用JQuery,這僅僅是由於對於咱們如今的討論,它不是一個咱們真正關心的細節。這些代碼中不關心你用哪一個JS框架(JQuery,Dojo,YUI等等)來解決如此無趣的問題。
讓咱們來看看,在沒有任何「類」幫助庫或語法的狀況下,咱們如何用經典風格的純JS來實現「類」設計:
OO設計模式告訴咱們要在父類中聲明一個基礎render(..),以後在咱們的子類中覆蓋它,但不是徹底替代它,而是用按鈕特定的行爲加強這個基礎功能。
注意 顯示假想多態 的醜態,Widget.call和Widget.prototype.render.call引用是爲了假裝從子「類」方法獲得「父類」基礎方法支持的「super」調用。呃。
ES6 class 語法糖
咱們會在附錄A中講解ES6的class語法糖,可是讓咱們演示一下咱們如何用class來實現相同的代碼。
毋庸置疑,經過使用ES6的class,許多前面經典方法中語法的醜態被改善了。super(..)的存在看起來很是適宜(但當你深刻挖掘它時,不全是好事!)。
除了語法上的改進,這些都不是 真正的 類,由於他們仍然工做在[[Prototype]]機制之上。它們依然會受到思惟模型不匹配的拖累,就像咱們在第四,五章中,和直到如今探索的那樣。附錄A將會詳細講解ES6class語法和他的含義。咱們將會看到爲何解決語法上的小問題不會實質上解決咱們在JS中的類的困惑,雖然它作出了勇敢的努力僞裝解決了問題!
不管你是使用經典的原型語法仍是新的ES6語法糖,你依然選擇了使用「類」來對問題(UI部件)進行建模。正如咱們前面幾章試着展現的,在JavaScript中作這個選擇會帶給你額外的頭疼和思惟上的彎路。
委託部件對象
這是咱們更簡單的Widget/Button例子,使用了 OLOO風格委託:
使用這種OLOO風格的方法,咱們不認爲Widget是一個父類而Button是一個子類,Wedget只是一個對象 和某種具體類型的部件也許想要代理到的工具的集合,並且Button也只是一個獨立的對象(固然,帶有委託至Wedget的連接!)。
從設計模式的角度來看,咱們 沒有 像類的方法建議的那樣,在兩個對象中共享相同的render(..)方法名稱,而是選擇了更能描述每一個特定任務的不一樣的名稱。一樣的緣由,初始化 方法被分別稱爲init(..)和setup(..)。
不只委託設計模式建議使用不一樣並且更具描述性的名稱,並且在OLOO中這樣作會避免難看的顯式假想多態調用,正如你能夠經過簡單,相對的this.init(..)和this.insert(..)委託調用看到的。
語法上,咱們也沒有任何構造器,.prototype或者new出現,它們事實上是沒必要要的設計。
如今,若是你再細心考察一下,你可能會注意到以前僅有一個調用(var btn1 = new Button(..)),而如今有了兩個(var btn1 = Object.create(Button)和btn1.setup(..))。這猛地看起來像是一個缺點(代碼變多了)。
然而,即使是這樣的事情,和經典原型風格比起來也是 OLOO風格代碼的優勢。爲何?
用類的構造器,你「強制」(不徹底是這樣,可是被強烈建議)構建和初始化在同一個步驟中進行。然而,有許多種狀況,可以將這兩步分開作(就像你在OLOO中作的)更靈活。
舉個例子,咱們假定你在程序的最開始,在一個池中建立全部的實例,但你等到在它們被從池中找出並使用以前再用指定的設置初始化它們。咱們的例子中,這兩個調用緊挨在一塊兒,固然它們也能夠按須要發生在很是不一樣的時間和代碼中很是不一樣的部分。
OLOO 對關注點分離原則有 更好 的支持,也就是建立和初始化沒有必要合併在同一個操做中。
更簡單的設計
OLOO除了提供表面上更簡單(並且更靈活!)的代碼以外,行爲委託做爲一個模式實際上會帶來更簡單的代碼架構。讓咱們講解最後一個例子來講明OLOO是如何簡化你的總體設計的。
這個場景中咱們將講解兩個控制器對象,一個用來處理網頁的登陸form(表單),另外一個實際處理服務器的認證(通訊)。
咱們須要幫助工具來進行與服務器的Ajax通訊。咱們將使用JQuery(雖然其餘的框架均可以),由於它不只爲咱們處理Ajax,並且還返回一個相似Promise的應答,這樣咱們就能夠在代碼中使用.then(..)來監聽這個應答。
注意: 咱們不會再這裏講到Promise,但咱們會在之後的 你不懂JS 系列中講到。
根據典型的類的設計模式,咱們在一個叫作Controller的類中將任務分解爲基本功能,以後咱們會衍生出兩個子類,LoginController和AuthController,它們都繼承自Controller並且特化某些基本行爲。
咱們有全部控制器分享的基本行爲,它們是success(..),failure(..)和showDialog(..)。咱們的子類LoginController和AuthController覆蓋了failure(..)和success(..)來加強基本類的行爲。還要注意的是,AuthController須要一個LoginController實例來與登陸form互動,因此它變成了一個數據屬性成員。
另一件要提的事情是,咱們選擇一些 合成 散佈在繼承的頂端。AuthController須要知道LoginController,因此咱們初始化它(new LoginController()),使它一個成爲this.login的類屬性成員來引用它,這樣AuthController才能夠調用LoginController上的行爲。
注意: 這裏可能會存在一絲衝動,就是使AuthController繼承LoginController,或者反過來,這樣的話咱們就會經過繼承鏈獲得 虛擬合成。可是這是一個很是清晰地例子,代表對這個問題來說,將類繼承做爲模型有什麼問題,由於AuthController和LoginController都不特化對方的行爲,因此它們之間的繼承沒有太大的意義,除非類是你惟一的設計模式。與此相反的是,咱們在一些簡單的合成中分層,而後它們就能夠合做了,同時他倆都享有繼承自父類Controller的好處。
若是你熟悉面向類(OO)的設計,這都聽該看起來十分熟悉和天然。
去類化
可是,咱們真的須要用一個父類,兩個子類,和一些合成來對這個問題創建模型嗎?有辦法利用OLOO風格的行爲委託獲得 簡單得多 的設計嗎?有的!
由於AuthController只是一個對象(LoginController也是),咱們不須要初始化(好比new AuthController())就能執行咱們的任務。全部咱們要作的是:
固然,經過OLOO,若是你確實須要在委託鏈上建立一個或多個附加的對象時也很容易,並且仍然不須要任何像類實例化那樣的東西:
使用行爲委託,AuthController和LoginController僅僅是對象,互相是 水平 對等的,並且沒有被安排或關聯成面向類中的父與子。咱們有些隨意地選擇讓AuthController委託至LoginController —— 相反方向的委託也一樣是有效的。
第二個代碼段的主要要點是,咱們只擁有兩個實體(LoginController and AuthController),而 不是以前的三個。
咱們不須要一個基本的Controller類來在兩個子類間「分享」行爲,由於委託是一種能夠給咱們所需功能的,足夠強大的機制。同時,就像以前注意的,咱們也不須要實例化咱們的對象來使它們工做,由於這裏沒有類,只有對象自身。 另外,這裏不須要 合成 做爲委託來給兩個對象 差別化 地合做的能力。
最後,因爲沒有讓名稱success(..)和failure(..)在兩個對象上相同,咱們避開了面向類的設計的多態陷阱:它將會須要難看的顯式假想多態。相反,咱們在AuthController上稱它們爲accepted()和rejected(..) —— 對於他們的具體任務來講,稍稍更具描述性的名稱。
底線: 咱們最終獲得了相同的結果,可是用了(顯著的)更簡單的設計。這就是OLOO風格代碼和 行爲委託 設計模式的力量。
更好的語法
一個使ES6class看似如此誘人的更好的東西是(見附錄A來了解爲何要避免它!),聲明類方法的速記語法:
咱們從聲明中扔掉了單詞function,這使全部的JS開發者歡呼!
你可能已經注意到,並且爲此感到沮喪:上面推薦的OLOO語法出現了許多function,這看起來像對OLOO簡化目標的詆譭。但它沒必要是!
在ES6中,咱們能夠在任何字面對象中使用 簡約方法聲明,因此一個OLOO風格的對象能夠用這種方式聲明(與class語法中相同的語法糖):
惟一的區別是字面對象的元素間依然須要,逗號分隔符,而class語法沒必要如此。這是在整件事情上很小的讓步。
還有,在ES6中,一個你使用的更笨重的語法(好比AuthController的定義中):你一個一個地給屬性賦值而不使用字面對象,能夠改寫爲使用字面對象(因而你可使用簡約方法),並且你可使用Object.setPrototypeOf(..)來修改對象的[[Prototype]],像這樣:
ES6中的OLOO風格,與簡明方法一塊兒,變得比它之前 友好得多(即便在之前,它也比經典的原型風格代碼簡單好看的多)。 你沒必要非得選用類(複雜性)來獲得乾淨漂亮的對象語法!
沒有詞法
簡約方法確實有一個缺點,一個重要的細節。考慮這段代碼:
這是去掉語法糖後,這段代碼將如何工做:
看到區別了?bar()的速記法變成了一個附着在bar屬性上的 匿名函數表達式(function()..),由於函數對象自己沒有名稱標識符。和擁有詞法名稱標識符baz,附着在.baz屬性上的手動指定的 命名函數表達式(function baz()..)作個比較。
那又怎麼樣?在 「你不懂JS」 系列的 「做用域與閉包」 這本書中,咱們詳細講解了 匿名函數表達式 的三個主要缺點。咱們簡單地重複一下它們,以便於咱們和簡明方法相比較。
一個匿名函數缺乏name標識符:
使調試時的棧追蹤變得困難
使自引用(遞歸,事件綁定等)變得困難
使代碼(稍稍)變得難於理解
第一和第三條不適用於簡明方法。
雖然去掉語法糖使用 匿名函數表達式 通常會使棧追蹤中沒有name。簡明方法在語言規範中被要求去設置相應的函數對象內部的name屬性,因此棧追蹤應當可使用它(這是依賴於具體實現的,因此不能保證)。
不幸的是,第二條 仍然是簡明方法的一個缺陷。 它們不會有詞法標識符用來自引用。考慮:
在這個例子中上面的手動Foo.bar(x*2)引用就足夠了,可是在許多狀況下,一個函數不必可以這樣作,好比使用this綁定,函數在委託中被分享到不一樣的對象,等等。你將會想要使用一個真正的自引用,而函數對象的name標識符是實現的最佳方式。
只要當心簡明方法的這個注意點,並且若是當你陷入缺乏自引用的問題時,僅僅爲這個聲明 放棄簡明方法語法,取代以手動的 命名函數表達式 聲明形式:baz: function baz(){..}。
自省
若是你花了很長時間在面向類的編程方式(無論是JS仍是其餘的語言),你可能會對 類型自省 很熟悉:自省一個實例來找出它是什麼 種類 的對象。在類的實例上進行 類型自省 的主要目的是根據 對象是如何建立的 來推斷它的結構/能力。
考慮這段代碼,它使用instanceof(見第五章)來自省一個對象a1來推斷它的能力:
由於Foo.prototype(不是Foo!)在a1的[[Prototype]]鏈上(見第五章),instanceof操做符(令人困惑地)僞裝告訴咱們a1是一個Foo「類」的實例。有了這個知識,咱們假定a1有Foo「類」中描述的能力。
固然,這裏沒有Foo類,只有一個普通的函數Foo,它剛好擁有一個引用指向一個隨意的對象(Foo.prototype),而a1剛好委託連接至這個對象。經過它的語法,instanceof僞裝檢查了a1和Foo之間的關係,但它實際上告訴咱們的是a1和Foo.prototype(這個隨意被引用的對象)是否有關聯。
instanceof在語義上的混亂(和間接)意味着,要使用以instanceof爲基礎的自省來查詢對象a1是否與討論中的對象有關聯,你 不得不 擁有一個持有對這個對象引用的函數 —— 你不能直接查詢這兩個對象是否有關聯。
回想本章前面的抽象Foo / Bar / b1例子,咱們在這裏縮寫一下:
爲了在這個例子中的實體上進行 類型自省, 使用instanceof和.prototype語義,這裏有各類你可能須要實施的檢查:
能夠說,其中有些爛透了。舉個例子,直覺上(用類)你可能想說這樣的東西Bar instanceof Foo(由於很容易混淆「實例」的意義認爲它包含「繼承」),但在JS中這不是一個合理的比較。你不得不說Bar.prototype instanceof Foo。
另外一個常見,但也許健壯性更差的 類型自省 模式叫「duck typing(鴨子類型)」,比起instanceof來許多開發者都傾向於它。這個術語源自一則諺語,「若是它看起來像鴨子,叫起來像鴨子,那麼它必定是一隻鴨子」。
例如:
與其檢查a1和一個持有可委託的something()函數的對象的關係,咱們假設a1.something測試經過意味着a1有能力調用.something()(無論是直接在a1上直接找到方法,仍是委託至其餘對象)。就其自己而言,這種假設沒什麼風險。
可是「鴨子類型」經常被擴展用於 除了被測試關於對象能力之外的其餘假設,這固然會在測試中引入更多風險(好比脆弱的設計)。
「鴨子類型」的一個值得注意的例子來自於ES6的Promises(就是咱們前面解釋過,將再也不本書內涵蓋的內容)。
因爲種種緣由,須要斷定任意一個對象引用是否 是一個Promise,但測試是經過檢查對象是否剛好有then()函數出如今它上面來完成的。換句話說,若是任何對象 剛好有一個then()方法,ES6的Promises將會無條件地假設這個對象 是「thenable」 的,並且所以會指望它按照全部的Promises標準行爲那樣一致地動做。
若是你有任何非Promise對象,而卻無論由於什麼它剛好擁有then()方法,你會被強烈建議使它遠離ES6的Promise機制,來避免破壞這種假設。
這個例子清楚地展示了「鴨子類型」的風險。你應當僅在可控的條件下,保守地使用這種方式。
再次將咱們的注意力轉向本章中出現的OLOO風格的代碼,類型自省 變得清晰多了。讓咱們回想(並縮寫)本章的Foo / Bar / b1的OLOO示例:
使用這種OLOO方式,咱們所擁有的一切都是經過[[Prototype]]委託關聯起來的普通對象,這是咱們可能會用到的大幅簡化後的 類型自省:咱們再也不使用instanceof,由於它使人迷惑地僞裝與類有關係。如今,咱們只須要(非正式地)問這個問題,「你是個人 一個 原型嗎?」。再也不須要用Foo.prototype或者痛苦冗長的Foo.prototype.isPrototypeOf(..)來間接地查詢了。
我想能夠說這些檢查比起前面一組自省檢查,極大地減小了複雜性/混亂。又一次,咱們看到了在JavaScript中OLOO要比類風格的編碼簡單(但有着相同的力量)。
複習
在你的軟件體系結構中,類和繼承是你能夠 選用 或 不選用 的設計模式。多數開發者理所固然地認爲類是組織代碼的惟一(正確的)方法,但咱們在這裏看到了另外一種不太常被提到的,但實際上十分強大的設計模式:行爲委託。
行爲委託意味着對象彼此是對等的,在它們本身當中相互委託,而不是父類與子類的關係。JavaScript的[[Prototype]]機制的設計本質,就是行爲委託機制。這意味着咱們能夠選擇掙扎着在JS上實現類機制,也能夠欣然接受[[Prototype]]做爲委託機制的本性。
當你僅用對象設計代碼時,它不只能簡化你使用的語法,並且它還能實際上引領更簡單的代碼結構設計。
OLOO(連接到其餘對象的對像)是一種沒有類的抽象,而直接建立和關聯對象的代碼風格。OLOO十分天然地實現了基於[[Prototype]]的行爲委託。