JS與面向對象
本篇不討論語法,我們討論偏思想的東西。
什麼是面向對象?
首先,面向對象並不是說你寫一個class就是面向對象了。在Java裡面Everything is class,全部都是Class,還有React也需要寫class,所以很多人寫class並不是他自己要寫class,而是編程語言或者框架要求他寫class。因此就會存在一個窘境,如下圖所示:
雖然是寫的class,但是代碼風格是面向結構的,只是套了一個class的外衣,真正面向對象的是所使用的框架。
所以面向對象應該是一種思想,而不是你代碼的組織形式,甚至有時候你連一個class都沒寫。
面向對象的英文為Object Oriented,它的準確翻譯應該為「面向物件」,而不是「面向對象」,只不過不知道是誰翻譯了這麼一個看似「高大上」但是不符合實際的名詞。面向對象是對世界物件的抽象和封裝,例如車子、房子和狗等。
面向對象的特點
面向對象有三個主要的特點:封裝、繼承和多態。
1
封裝
現在我要研究下狗,並且關注它的叫和咬人行為,所以我封裝了一個狗的類,如下代碼所示:
上面代碼封裝兩個行為:叫、咬人,和一個屬性:年齡。
2
繼承
然後我又要研究下哈士奇,如下圖所示:
哈士奇是狗的一種,我讓它繼承了Dog這個類,於是它就繼承了父類的行為,如它可以咬你:
同時,哈士奇它有自己的行為,例如它可能時不時就會露出奇怪的表情。
3
多態
哈士奇也會叫,但是它不是「汪汪汪」地叫,它有時候會發出像狼嚎的聲音,所以同樣是叫的行為,但是哈士奇有自己特點,這個就是多態,如下所示:
當調用Husky的bark函數時就是wolf wolf而不是wang wang了:
面向對象的實際例子
上傳進度條
一個頁面會有多個上傳圖片的地方,每個上傳地方都會生成一個進度條,如下圖所示:
所以考慮把進度條封裝成一個類ProgressBar,如下代碼所示:
ProgressBar封裝了設置進度、完成、失敗的函數,這個就是面向對象的封裝。
最後的addFailedText函數是內部的實現,不希望實例直接調用,也就是說它應該是一個私有的、對外不可見的函數。
但是由於JS沒有私有屬性、私有函數的概念,所以還是可以調的,如果要實現私有屬性得通過閉包之類的技巧實現。
接著我想做一個帶有百分比數字的進度條,如下圖所示:
於是我想到了面向對象的繼承,寫一個
ProgressBarWithNumber的類,繼承ProgressBar,如下代碼所示:
子類繼承了父類的函數,同時覆蓋/實現了父類的某些行為。上面的setProgress函數既體現了多態又體現了繼承。
再舉一個例子,HTML元素的繼承關係。
HTML元素的繼承關係
如下圖所示:
P標籤是用一個HTMParaphElement的類表示,這個類繼承關係往上有好幾層,最上層是Node類,Node又組合TreeScope,TreeScope標明當前Node結點是屬於哪個document的(一個頁面可能會嵌入iframe)。
繼承和組合
繼承是為了實現復用,組合其實也是為了實現復用。繼承是is-a的關係,而組合是has-a的關係。可以把上面的ProgressBar改成組合的方式,如下代碼所示:
在構造函數裡面組合了一個progressBar的實例,然後在setProgress函數裡面利用這個實例去設置進度條的百分比。
也就是說帶有數字的進度條裡面有一條普通的進度條,這是組合,而當我們用繼承的時候就變成了帶數字的進度條是一種進度條。
這兩個都說得通,但是上面HTML元素的例子裡面,可以說一個Node結點有一個TreeScope,但是不能說Node結點是一個TreeScope。
那麼是繼承好用一點,還是組合好用一點呢?
在《Effective Java》裡面有一個條款:
Item 16 : Favor composition over inheritance
意思為偏向於使用組合而非繼承,為什麼說組合比較好呢?因為繼承的耦合性要大於組合,組合更加靈活。
繼承是編譯階段就決定了關係,而組合是運行階段才決定關係。組合可以組合多個,而如果要搞多重繼承系統的複雜性無疑會大大增加。
就上面的進度條的例子來說,使用組合會比使用繼承的方式好嗎?假設某一天,帶數字的進度條不想復用普通的進度條了,要復用另外一種類型的進度條,使用繼承就得改它的繼承關係,萬一帶數字的進度條還派生了另外一個類,這個孫子類如果剛好用了普通進度條的一個函數,那這個條鏈就斷了,導致孫子類也要改。所以可以看出組合的方式更加簡易,繼承相對比較複雜。
但是如果要我在這之上加一個條款的話我會這麼加:
Item 0: Favor Simple Ways over OOP
因為能用簡單的方式解決問題就應該用簡單的方式,而不是一著手就是各種面向對象的繼承、多態的思想,帶數字的LoadingBar其實不需要使用繼承或者組合,只要帶一個參數控制就好了,是否要顯示數字。
筆者認為應該先使用簡潔的方式解決問題,然後再考慮性能、代碼組織優化等。為了5%的效果,增加了系統50%的複雜度,其實不值得,除非那個問題是瓶頸問題,能夠提升一點是一點。為了寫一個小需求,封裝了幾十個類,最後需求一變這幾十個類就都沒用了。
接著重點說一下設計模式和OOP的編程原則。
面向對象編程原則和設計模式
單例模式
單例是一種比較簡單也是比較常見的模式。例如現在要定義Task類,要實現它的單例,因為全局只能有一個數組存放Task,如果有任務就都放到這個隊列裡面,按先進先出的順序執行。
於是我先寫一個Task類:
現在要實現它的單例,可以這麼實現:
每次get的時候先判斷有mapTask有沒有Task的實例了,如果沒有則為第一次,先去實例化一個,並做些初始化工作,如果有則直接返回。然後執行mapTask.get()的時候就能夠保證獲取到的是一個單例。
但是這種實現其實不太安全,任何人可通過設置:
去破壞你這個單例,那怎麼辦呢?一方面JS本身沒有私有屬性,另一方面要怎麼解決留給讀者去思考。
因為JS的Object本身就是單例的,所以可以把Task類改成一個taskWorker,如下代碼所示:
顯然第二種方式比較簡單,但是它只能有一個全局的task。而第一種辦法可以擁有幾種不同業務的Task,不同業務互不影響。例如除了mapTask之外,還可以再寫一個searchTask的業務。
策略模式
這個例子已經提過很多次,這裡再簡單提一下。假設現在要彈幾個註冊的框,每個註冊的框只是頂部的文案不一樣,而其他地方包括邏輯等都一樣,所以,我就把文案當作一個個的策略,使用的時候根據不同的類型,映射到不同的策略,如下圖所示:
註冊完成後需要去執行不同的操作,把這些操作也封裝成一個個的策略,同樣地根據不同的類型映射到不同的策略,如下圖所示:
這樣比寫if-else或者switch-case的好處就在於:如果以後要增加或者刪除某種類型的彈框,只需要去增刪一個type就可以了,而不用去改動if-else的邏輯。這個就叫做開閉原則——對修改是封閉的,而對擴展是開放的。
觀察者模式
觀察者模式也是經常和前端打交道的一種模式,事件監聽就是一種觀察者模式,如下實現一個觀察者模式:
觀察者向消息的接收者訂閱消息,一旦接收者收到消息後就把消息下發給它的觀察者們。在一個地圖繪製搜索的應用裡面,點擊最後一個點關閉路徑,要觸發搜索:
但其實不用再去手動調搜索的介面了,因為地圖本身就監聽了drag_end事件,在這個事件裡面會去搜索,所以在繪製完成之後只要執行:
就可以了,即給drag_end事件的觀察者們下發一個消息,讓它們去執行。
適配器模式
在一個響應式的頁面裡面,假設小屏和大屏顯示的分頁樣式不一樣,小屏要這樣顯示:
而大屏要這樣顯示:
它們初始化和更新狀態的函數都不一樣,如下所示:
如果我每次用的時候都得先判斷一下不同的屏幕大小然後去調不同的函數就顯得有點麻煩,所以可以考慮用一個適配器,對外提供統一的介面,如下所示:
使用者只要調一下paginationAdapter.showPage就可以更新分頁狀態,它不需要去關心當前是大屏還是小屏,由適配器去處理這些細節。
工廠模式
工廠模式是把創建交給一個「工廠」,使用者無需要關心創建細節,如下代碼所示:
需要哪種類型的Task的時候就傳一個類型或者產品名字給一個工廠,工廠根據名字去生產相應的產品給我,而我不需要關心它是怎麼創建的,要不要單例之類的。
外觀/門面模式
在一個搜索邏輯裡面,為了顯示搜索結果需要執行以下這麼多個操作:
於是考慮用一個模塊把它包起來,如下圖所示:
把那麼多個操作封裝成一個模塊,對外只提供一個門面叫showResult,使用者只要調一下這個showResult就可以了,它不需要知道究竟要怎麼去顯示結果。
狀態模式
現在要實現一個發twitter的消息框,要求是當字數為0或者超過140的時候,發推按鈕不可點擊,並且剩餘字數會跟著變,如下圖所示:
我想用一個state來保存當前的狀態,然後當用戶輸入的時候,這個state的數據會跟著變,同時更新發推按鈕的狀態,如下代碼所示:
用一個state保存當前的狀態,通過獲取當前state進行下一步的操作。
可以把它改得更加智能一點,即在上面setState的時候,自動去更新DOM,如下代碼所示:
然後還可以再做得更智能,狀態變的時候自動去比較當前狀態所渲染的虛擬DOM和真實DOM的區別,自動去改變真實DOM,如下代碼示:
這個其實就是React的原型,不同的狀態有不同的表現行為,所以可以認為是一個狀態模式,並且通過狀態去驅動DOM的更改。
代理模式
如下圖所示:
使用React不直接操作DOM,而是把數據給State,然後委託給State和虛擬DOM去操作真實DOM,所以它又是一個代理模式。
狀態模式的另一個例子
React的那個例子並不是很典型,這裡再舉一個例子,如下代碼所示,改變一個房源的狀態:
改一個房源的狀態之前先要判斷一下當前的狀態,如果當前狀態不支持的話那麼不允許修改,要是像上面那樣寫的話就得寫好多個if-else,我們可以用狀態模式重構一下。
你會發現狀態模式和策略模式是孿生兄弟,它們的形式相同,只是目的不同,一個是封裝成策略,一個是封裝成狀態。這樣的代碼就比寫很多個if-else強多了,特別是當狀態切換關係比較複雜的時候。
裝飾者模式
要實現一個貸款的計算器,如下圖所示:
點了計算的按鈕之後,除了要計算結果,還要把結果發給後端做一個埋點,所以寫了一個calculateResult的函數:
因為要把結果返回出來,所以這個函數有兩個功能,一個是計算結果,第二個是改變DOM,這樣寫在一起感覺不太好。那怎麼辦呢?
我們把這個函數拆了,首先有一個LoanCalculator的類專門負責計算小數的結果,如下代碼所示:
它還提供了一個getResult的函數,如果結果沒算過那先算一下保存起來,如果已經算過了那就直接用算好的那個。
然後再寫一個NumberFormater,它負責把小數結果格式化成帶逗號的形式:
在它的構造函數裡面傳一個calculator給它,這個calculator可以是上面的LoanCalculator,獲取到它的計算結果然後格式化。
接著寫一個DOMRenderer的類,它負責把結果顯示出來:
最後可以這麼用:
可以看到它就是一個裝飾的過程,一層一層地裝飾,如下圖所示:
下一個裝飾者調上一個的calResult函數,對它的結果進一步地裝飾。如果這些裝飾者的返回結果類型比較平行的時候,可以一層層地裝飾下去。
使用裝飾者模式,邏輯是清晰了,代碼看起來高大上了,但是系統複雜性增加了,有時候能用簡單的還是先用簡單的方式實現。
總結一下本文提到的面向對象的編程原則:
把共性和特性或者會變和不變的分離出來
少用繼承,多用組合
低耦高聚
開閉原則
單一職責原則
最後,如果遇到一個問題你先查一下有哪個設計模式或者有哪個原則可以指導和解決這個問題,那你就被套路套住了。
功夫學到最後應該是忘掉所有的招數,做到心中無法,隨心所欲,拈手就來,這才是最高境界。相反地,你會發現那種整天高喊各種原則、各種理論的人,其實很多時候他自己也沒實踐過,只是在空喊口號。
作者:會編程的銀豬
文章來源:
http://www.renfed.com/2017/05/21/js-oop/
※10個JavaScript概念!Node.js程序員必須掌握
※Vue 用戶的 React 上手小結
※ui 設計中的插畫與情感化設計
※快速學會SVN的搭建和使用
※大白話解釋 Git和GitHub
TAG:優才學院 |
※做一次面向對象的體操:將 JSON 字元串轉換為嵌套對象的一種方法
※面向不同需求的對象存儲系統對比:Ceph與Swift
※php對面向對象的支持,完全可以開發大型商城網站
※撲克牌的面向對象建模
※在線等!如何用對象能懂的方式解釋面向對象編程?
※3.面向對象(三)
※面向對象編程之興衰
※面向對象:待盼君來,從此與君同在
※Python指南:面向對象程序設計
※面向對象編程已死,OOP 永存!
※面向對象編程,再見!
※面向對象:對方申請同步您的世界,是否授權?
※面向對象:你願意和我一起去尋找 ALL BLUE 嗎?
※東芝存儲器株式會社將推出面向SATA應用的Value SAS SSD
※新iPhone全面曝光,將推兩款支持雙卡雙待的機型面向中國市場!
※面向對象:最浪漫的事就是我在鬧你在笑
※面向對象:希望找一位穩重坦蕩的大哥哥,走向遠方
※Python面向對象之魔法方法/雙下方法
※面向對象:王者,吃雞,等你保護
※向日葵一直面向太陽,YES or NO?