JavaScript 設(shè)計(jì)模式學(xué)習(xí)總結(jié)與感悟(開(kāi)發(fā)&面試必備)
作者:吃葡萄不吐番茄皮
原文地址:https://segmentfault.com/a/1190000019663847
前言
最近閱讀了《JavaScript設(shè)計(jì)模式與開(kāi)發(fā)實(shí)踐》,收獲頗豐,于是想寫(xiě)一點(diǎn)總結(jié)及感想

寫(xiě)一篇文章對(duì)于我的意義在于:
給別人講述知識(shí)時(shí)可以發(fā)現(xiàn)自己掌握的是否牢固透徹,寫(xiě)的過(guò)程不斷發(fā)現(xiàn)自己的不足,然后通過(guò)一些方式來(lái)解決問(wèn)題,這也是一種學(xué)習(xí)過(guò)程;當(dāng)然,寫(xiě)文章給別人看,也要從讀者的角度出發(fā),考慮他們想要從這篇文章獲得什么,還有就是你想表達(dá)些什么給讀者
這種過(guò)程大概叫費(fèi)曼學(xué)習(xí)法,圖解:
(圖片來(lái)自網(wǎng)絡(luò),侵刪)
這篇文章我想表達(dá)的是:學(xué)習(xí)設(shè)計(jì)原則設(shè)計(jì)模式的好處、介紹設(shè)計(jì)原則和設(shè)計(jì)模式、常用設(shè)計(jì)模式的實(shí)踐、代碼重構(gòu)的具體方法、一些問(wèn)題一些思考。你可以先讀一遍帶著疑問(wèn)去閱讀這本書(shū)籍或者閱讀完書(shū)籍再來(lái)看這篇文章是否有助于你理解
為什么要學(xué)習(xí)設(shè)計(jì)原則、設(shè)計(jì)模式
首先,設(shè)計(jì)原則、設(shè)計(jì)模式受用的目標(biāo)人群我覺(jué)得是有一定的js基礎(chǔ)且有一定的項(xiàng)目實(shí)踐經(jīng)歷的開(kāi)發(fā)者,不然的話,就算學(xué)習(xí)設(shè)計(jì)也是生搬硬套,收貨甚微,當(dāng)有了一定基礎(chǔ)及實(shí)踐之后,閱讀本書(shū)之后有三種感覺(jué):

你的某些代碼就是書(shū)上的反例,醍醐灌頂?shù)母杏X(jué) 你的某些代碼已經(jīng)實(shí)踐了某些設(shè)計(jì)模式遵從了某些設(shè)計(jì)原則,但是你并不知道這樣寫(xiě)代碼是叫這個(gè)模式以及這個(gè)模式的全部?jī)?yōu)缺點(diǎn)或者你的代碼還有更進(jìn)一步優(yōu)化的空間 內(nèi)心冷笑一聲:哼 so easy... emmmmm,如果大佬還愿意繼續(xù)閱讀本文的話,希望大佬可以在評(píng)論區(qū)指點(diǎn)一二
個(gè)人認(rèn)為,JavaScript設(shè)計(jì)原則以及設(shè)計(jì)模式都只屬于軟件設(shè)計(jì)的一部分,但這意味著已經(jīng)開(kāi)始脫離了API調(diào)用工程師的稱(chēng)號(hào),開(kāi)始接觸編程思想,但是設(shè)計(jì)原則跟模式有限,只針對(duì)于代碼層面。
打個(gè)比方:vue源碼使用了xx模式,解決了xx問(wèn)題,但是,在選擇xx模式解決xx問(wèn)題的背后又有多少思考?呢?簡(jiǎn)單猜測(cè)一下:一個(gè)框架or庫(kù) = 軟件設(shè)計(jì) + 其他框架優(yōu)點(diǎn)借鑒 + 創(chuàng)新 + 編碼 + 測(cè)試;本人水平有限,這是知識(shí)宏觀的揣測(cè)一下實(shí)現(xiàn)一個(gè)框架or庫(kù)需要的付出。請(qǐng)不要當(dāng)真or較真
可能沒(méi)人要你去寫(xiě)框架什么的,但是你負(fù)責(zé)的部分總是你來(lái)寫(xiě)來(lái)維護(hù),不按套路出牌受傷的是自己
舉這個(gè)例子是想說(shuō)明:學(xué)習(xí)一下設(shè)計(jì)原則設(shè)計(jì)模式是多么的有必要(強(qiáng)行解釋?zhuān)顬橹旅?/p>
二、JavaScript常用設(shè)計(jì)原則
我想提一個(gè)這本書(shū)的缺點(diǎn),就是目錄,設(shè)計(jì)模式都是要遵循設(shè)計(jì)原則的,而且很多設(shè)計(jì)模式章節(jié)都提到了設(shè)計(jì)原則,然而書(shū)的目錄是最后一個(gè)大章節(jié)才說(shuō)的設(shè)計(jì)原則,我覺(jué)得設(shè)計(jì)原則應(yīng)該放在設(shè)計(jì)模式之前,所以建議先閱讀設(shè)計(jì)原則再閱讀設(shè)計(jì)模式,在理解上也會(huì)有幫助,至于第一章節(jié)的基礎(chǔ)知識(shí)介紹,這個(gè)看各人情況可以選擇要不要忽略
下面介紹常用的設(shè)計(jì)原則
1. 單一職責(zé)原則 SRP
【定義】
單一職責(zé)原則的定義是‘引起變化的原因’,如果你有兩個(gè)原因去修改一個(gè)方法,說(shuō)明這個(gè)方法有兩個(gè)職責(zé),承擔(dān)的職責(zé)越多你需要修改他的可能性就越大,修改代碼總是件危險(xiǎn)的事情,特別是兩個(gè)職責(zé)耦合在一起的時(shí)候一句話概括:一個(gè)對(duì)象(方法)只做一件事
【理解】
書(shū)上有介紹單一職責(zé)原則相關(guān)的模式,其實(shí)我覺(jué)得從原則去聯(lián)系模式有點(diǎn)不合理,因?yàn)樗心J蕉紩?huì)去遵從設(shè)計(jì)原則,知識(shí)側(cè)重點(diǎn)不一樣而已。所以我會(huì)在下個(gè)章節(jié)的模式里去聯(lián)系原則,而說(shuō)原則,我只想脫離模式單獨(dú)去說(shuō)原則的優(yōu)缺點(diǎn)以及如何應(yīng)用
【優(yōu)點(diǎn)】降低了單個(gè)類(lèi)或者對(duì)象的復(fù)雜度,按照職責(zé)把對(duì)象分解成更小的粒度,這有助于代碼的復(fù)用,也有利于進(jìn)行單元測(cè)試。當(dāng)一個(gè)職責(zé)需要變更的時(shí)候,不會(huì)影響到其他的職責(zé)。
【缺點(diǎn)】增加編碼復(fù)雜度,同時(shí)增加對(duì)象之間聯(lián)系的難度
【應(yīng)用】煮個(gè)栗子,js的組件化開(kāi)發(fā),有一種思想就是組件的原子化,把組件拆到不能再拆的粒度,是最好維護(hù)的。但事實(shí)真的是這樣嗎,組件多了,組件之間的聯(lián)系也多了,就意味需要管理更多的組件及組件間的復(fù)雜關(guān)系,有時(shí)候明明兩個(gè)組件像孿生兄弟一樣分不開(kāi),你偏要拆散他們并且給他們一座橋梁去聯(lián)系,這無(wú)疑比讓他們?cè)谝黄鸪蔀橐粋€(gè)整體要難維護(hù)的多,而且顆粒度越小,代表著開(kāi)發(fā)成本越大。
單一職責(zé)原則也是如此,這些思想其實(shí)建立在理想化的狀態(tài)上,而實(shí)際情況往往不會(huì)與理想對(duì)等,所以實(shí)際項(xiàng)目使用中,要學(xué)會(huì)靈活多變,知道什么時(shí)候其實(shí)是可以違反原則的,借用書(shū)上的話:
SRP 原則是所有原則中最簡(jiǎn)單也是最難正確運(yùn)用的原則之一。
要明確的是,并不是所有的職責(zé)都應(yīng)該一一分離
一方面,如果隨著需求的變化,有兩個(gè)職責(zé)總是同時(shí)變化,那就不必分離他們。比如在 ajax 請(qǐng)求的時(shí)候,創(chuàng)建 xhr 對(duì)象和發(fā)送 xhr 請(qǐng)求幾乎總是在一起的,那么創(chuàng)建 xhr 對(duì)象的職責(zé)和發(fā)送 xhr 請(qǐng)求的職責(zé)就沒(méi)有必要分開(kāi)。
另一方面,職責(zé)的變化軸線僅當(dāng)它們確定會(huì)發(fā)生變化時(shí)才具有意義,即使兩個(gè)職責(zé)已經(jīng)被耦 合在一起,但它們還沒(méi)有發(fā)生改變的征兆,那么也許沒(méi)有必要主動(dòng)分離它們,在代碼需要重構(gòu)的 時(shí)候再進(jìn)行分離也不遲
個(gè)人認(rèn)為這是理解最簡(jiǎn)單,實(shí)踐最難的一個(gè)
//?bad
var?createLoginLayer?=?(function(){
????var?div;
????return?function(){
????????if?(?!div?){
????????????div?=?document.createElement(?'div'?);
????????????div.innerHTML?=?'我是登錄浮窗';?
????????????div.style.display?=?'none';?
????????????document.body.appendChild(?div?);
????????}
????return?div;?}
})();
//?good
var?getSingle?=?function(?fn?){?//?獲取單例
????var?result;
????return?function(){
????????return?result?||?(?result?=?fn?.apply(this,?arguments?)?);
????}
var?createLoginLayer?=?function(){?//?創(chuàng)建登錄浮窗
????var?div?=?document.createElement(?'div'?);
????div.innerHTML?=?'我是登錄浮窗';
????document.body.appendChild(?div?);
????return?div;?
};
var?createSingleLoginLayer?=?getSingle(?createLoginLayer?);
var?loginLayer1?=?createSingleLoginLayer();
var?loginLayer2?=?createSingleLoginLayer();
alert?(?loginLayer1?===?loginLayer2?);?//?輸出:?true
2. 最少知識(shí)原則 LKP
單一職責(zé)原則說(shuō)道:一個(gè)對(duì)象(方法)只做一件事;那代表著我們要?jiǎng)?chuàng)建更多的對(duì)象(方法)來(lái)分解一個(gè)之前比較大的對(duì)象(方法),那分解之后,對(duì)象(方法)是小了,好維護(hù)好擴(kuò)展了,但是對(duì)象之間的聯(lián)系缺越來(lái)越多了,有兩個(gè)對(duì)象非常耦合,怎么辦
【定義】
最少知識(shí)原則要求我們?cè)谠O(shè)計(jì)程序時(shí),應(yīng)當(dāng)盡量減少對(duì)象之間的交互。如果兩個(gè)對(duì)象之間不 必彼此直接通信,那么這兩個(gè)對(duì)象就不要發(fā)生直接的相互聯(lián)系。常見(jiàn)的做法是引入一個(gè)第三者對(duì) 象,來(lái)承擔(dān)這些對(duì)象之間的通信作用。如果一些對(duì)象需要向另一些對(duì)象發(fā)起請(qǐng)求,可以通過(guò)第三 者對(duì)象來(lái)轉(zhuǎn)發(fā)這些請(qǐng)求
**【優(yōu)點(diǎn)】**減少或消除對(duì)象之間的耦合程度,提高復(fù)用性
**【缺點(diǎn)】**需要封裝對(duì)象或者引入一個(gè)第三者對(duì)象來(lái)處理兩者之間的聯(lián)系,有時(shí)候第三者對(duì)象會(huì)復(fù)雜到難以維護(hù)
**【應(yīng)用】**封裝是廣義上最少知識(shí)原則的一種體現(xiàn),封裝的好處是隱藏內(nèi)部屬性、方法、實(shí)現(xiàn)細(xì)節(jié),只拋出指定數(shù)據(jù)或?qū)ο蠼o外界,外界使用封裝對(duì)象的對(duì)象越少說(shuō)明聯(lián)系就越少。另外一種就是兩個(gè)對(duì)象很耦合的時(shí)候通過(guò)引入第三者來(lái)處理兩個(gè)對(duì)象之間的關(guān)系
這個(gè)demo是我自己編的,大概的意思是一個(gè)班級(jí)有n個(gè)學(xué)生,學(xué)生有自己的年齡,年齡相等的學(xué)生可以做朋友(emmmm,原諒我奇葩的腦洞,還好我沒(méi)說(shuō)性別一樣的才能做朋友,不然要被在座的各位打屎),如果每次新進(jìn)來(lái)一個(gè)學(xué)生,都要全班一個(gè)學(xué)生一個(gè)學(xué)生的問(wèn)年齡,那太麻煩了,如果老師入學(xué)的時(shí)候把學(xué)生按年齡分批了,那是不是很好找到,這個(gè)按年齡分批次就是引入的第三者來(lái)處理這個(gè)問(wèn)題,我不知道夠不夠形象啊,但是書(shū)上的外觀模式的代碼舉例真的辣雞,不夠形象?

let?nameList?=?[{
????name:?'eason',
????age:?1
??},?{
????name:?'taylor',
????age:?3
??},?{
????name:?'jack',
????age:?2
??},?{
????name:?'yu',
????age:?1
??},?{
????name:?'xixi',
????age:?3
??}]
??let?state?=?{}
??let?People?=?(name,?age)?=>?{
????let?that?=?{}
????that.name?=?name
????that.age?=?age
????that.friends?=?[]
????return?that
??}
??
??
//?bad?
for?(let?n?of?nameList)?{
????state[n.name]?=?new?People(n.name,?n.age)
??}
let?jay?=?new?People('jay',?3)
let?syz?=?new?People('syz',?2)
let?keys?=?Object.keys(state)
for?(let?k?of?keys)?{
????if?(state[k].age?===?jay.age)?{
??????jay.friends.push(state[k].name)
????}
????if?(state[k].age?===?syz.age)?{
??????syz.friends.push(state[k].name)
????}
??}
console.log('jay-friends',?jay.friends)?//?["jay",?"taylor",?"xixi"]
//good
let?ageList?=?[]
let?ageMap?=?{}
for?(let?n?of?nameList)?{
????state[n.name]?=?new?People(n.name,?n.age)
????if?(ageList.indexOf(n.age)?0)?{
??????ageList.push(n.age)
??????ageMap[n.age]?=?[]
??????ageMap[n.age].push(n.name)
????}?else?{
??????ageMap[n.age].push(n.name)
????}
??}
??let?addPeople?=?(name,?age)?=>?{
????ageMap[age]?=?ageMap[age]?||?[]
????ageMap[age].push(name)
????return?new?People(name,?age)
??}
??let?jay?=?addPeople('jay',?3)
??let?syz?=?addPeople('syz',?2)
??console.log('jay-friends',?ageMap[jay.age])?["taylor",?"xixi",?"jay"]
??console.log('syz-friends',?ageMap[syz.age])?["jack",?"syz"]
3. 開(kāi)放-封閉原則 OCP
【定義】
當(dāng)需要改變一個(gè)程序的功能或者給這個(gè)程序增加新功 能的時(shí)候,可以使用增加代碼的方式,但是不允許改動(dòng)程序的源代碼我來(lái)強(qiáng)行解釋一下,對(duì)擴(kuò)展開(kāi)放,對(duì)修改封閉
【優(yōu)點(diǎn)】
程序的穩(wěn)定性提升、容易變化的地方分離出來(lái)后更容易維護(hù)
【缺點(diǎn)】
代碼的完全封閉幾乎不可能,誰(shuí)也沒(méi)有’未卜先知‘的能力,但是我們可以盡可能的去容易變化和不容易變化的地方,挑出容易變化的地方進(jìn)行封閉
【應(yīng)用】
用對(duì)象的多態(tài)性消除條件分支(常用)
多態(tài)指的是:同一操作在不同的對(duì)象上展現(xiàn)出不同的結(jié)果 多態(tài)最根本的作用就是通過(guò)把過(guò)程化的條件分支語(yǔ)句轉(zhuǎn)化為對(duì)象的多態(tài)性,從而消除這些條件分支語(yǔ)句 我的理解是,只要有類(lèi)似的行為(map.show instanceof Function)就可以去執(zhí)行然后呈現(xiàn)出不同的結(jié)果,不必每個(gè)對(duì)象的’類(lèi)型‘
代碼對(duì)比:
//?bad
var?googleMap?=?{?
????show:?function(){
????console.log(?'開(kāi)始渲染谷歌地圖'?);?}
};
var?baiduMap?=?{?
????show:?function(){
????console.log(?'開(kāi)始渲染百度地圖'?);?}
};
var?renderMap?=?function(?type?){?
????if?(?type?===?'google'?){
?????googleMap.show();
????}else?if?(?type?===?'baidu'?){
?????baiduMap.show();
}?};
renderMap(?'google'?);?//?輸出:開(kāi)始渲染谷歌地圖?
renderMap(?'baidu'?);?//?輸出:開(kāi)始渲染百度地圖
//?good
var?renderMap?=?function(?map?){
????if?(?map.show?instanceof?Function?){
????map.show();?}
};
renderMap(?googleMap?);?//?輸出:開(kāi)始渲染谷歌地圖?
renderMap(?baiduMap?);?//?輸出:開(kāi)始渲染百度地圖
封裝變化
封裝變化就是把可能變化的地方分離出去,這樣只需要維護(hù)已經(jīng)被分離出去的容易變化的地方 方式有:放置掛鉤、回調(diào)函數(shù)等


原諒我懶...

此原則的相對(duì)性

總結(jié):開(kāi)放-封閉原則其實(shí)還是跟我們的程序的維護(hù)及擴(kuò)展性相關(guān)的原則,設(shè)計(jì)原則基本是圍繞程序的可讀、可維護(hù)、可擴(kuò)展、高性能、安全等方面來(lái)進(jìn)行的(正確性有待考究,目前我是這么認(rèn)為的,有不同意見(jiàn)歡迎評(píng)論區(qū)留言),設(shè)計(jì)原則這塊說(shuō)的都比較相對(duì),就想上一張的最少知識(shí)和單一職責(zé)原則一樣,實(shí)際項(xiàng)目想要的做到這些,只能是一定程度上的,因?yàn)橛行┰瓌t就像一個(gè)主觀題,是沒(méi)有標(biāo)準(zhǔn)答案的,但是有跡可循
三、常用設(shè)計(jì)模式
從【定義】【理解】【應(yīng)用】【優(yōu)點(diǎn)】【缺點(diǎn)】五個(gè)維度來(lái)介紹這些設(shè)計(jì)模式及應(yīng)用
1. 單例模式
【定義】
保證一個(gè)類(lèi)僅有一個(gè)實(shí)例,并提供一個(gè)訪問(wèn)它的全局訪問(wèn)點(diǎn)
【理解】
從字面意思來(lái)看,單例意思是單個(gè)實(shí)例。從定義來(lái)看,一個(gè)類(lèi)只有一個(gè)實(shí)例,但是對(duì)于JavaScript來(lái)說(shuō)創(chuàng)造一個(gè)對(duì)象so easy,不必像別的語(yǔ)言一樣必須先創(chuàng)建一個(gè)類(lèi)再通過(guò)類(lèi)創(chuàng)建對(duì)象,所以定義第一句對(duì)于js來(lái)說(shuō)無(wú)異于脫了褲子放p,我們只需要保證一個(gè)唯一的對(duì)象即可。(很多書(shū)籍都是比較早,在ES6之前發(fā)行的,所以都沒(méi)有類(lèi)這一說(shuō),所以所有JavaScript關(guān)于js無(wú)類(lèi)這一塊,大家可以自行思考,有類(lèi)的情況下應(yīng)該是怎樣的)為什么只能有一個(gè)實(shí)例?將全部職責(zé)集中在一個(gè)實(shí)例上(這種對(duì)象往往對(duì)比普通對(duì)象比較龐大復(fù)雜),這樣就能避免無(wú)謂的浪費(fèi)為什么要提供一個(gè)全局的訪問(wèn)點(diǎn)?因?yàn)橹挥幸粋€(gè)實(shí)例啊,所以要保證想用的地方都能訪問(wèn)到
【應(yīng)用】
個(gè)人覺(jué)得只要是那種使用相對(duì)頻繁、功能強(qiáng)大、創(chuàng)建一次相對(duì)消耗性能多的都可以考慮使用單例模式,這里圈起來(lái),要考!別以為自己有個(gè)有點(diǎn)復(fù)雜的對(duì)象就想說(shuō)使用單例模式,你可以先看下window對(duì)象有多少屬性多少方法還有一點(diǎn)就是,因?yàn)槲ㄒ坏?,而且?chuàng)建消耗一點(diǎn)性能,我們可以在需要的時(shí)候去判斷實(shí)例是否存在,不存在再創(chuàng)建,存在就直接使用,這種叫做惰性單例,比如xhr
【優(yōu)點(diǎn)】唯一實(shí)例,節(jié)約內(nèi)存開(kāi)銷(xiāo);
【缺點(diǎn)】
違背 單一職責(zé)原則,不好維護(hù)、不容易擴(kuò)展不適用于容易變化的實(shí)例 
2. 代理模式
【定義】
代理模式是為一個(gè)對(duì)象提供一個(gè)代用品或占位符,以便控制對(duì)它的訪問(wèn)
【理解】
明星有經(jīng)紀(jì)人作為代理,自身太忙無(wú)暇處理or不方便露面or...的事物都由經(jīng)紀(jì)人代為處理,明星可以跟代理人溝通(我要吃冰淇淋,我要演這個(gè)不演那個(gè)balabala),編程中的代理也是如此。代理分保護(hù)代理和虛擬代理,但是js中保護(hù)代理不容易實(shí)現(xiàn),以為常用的是虛擬代理
【應(yīng)用】
惰性加載,比如圖片懶加載 緩存代理,比如http304、Memoization,不懂Memoization的可以移步我的JavaScript性能優(yōu)化--算法和流程控制章節(jié)
【優(yōu)點(diǎn)】
分解本體職責(zé)--單一職責(zé)原則;解決本體暫時(shí)性無(wú)法處理一些請(qǐng)求;節(jié)約性能
【缺點(diǎn)】
編碼需要維護(hù)代理與本體的聯(lián)系 編碼需要保持代理和本體接口的一致性
3. 策略模式
【定義】
定義一系列的算法,把它們一個(gè)個(gè)封裝起來(lái),并且使它們可以相互替換
【理解】
把分支轉(zhuǎn)成配置,利用配置參數(shù)可以減少很多分支當(dāng)你的代碼里有很多if else或者switch的時(shí)候,就該考慮一下策略模式是否可以替換那些分支了為什么要用策略模式替換那些分支,一般情況下,我們把某個(gè)職責(zé)委托給一個(gè)策略對(duì)象,策略對(duì)象下的每一個(gè)鍵值對(duì)是一個(gè)分支,命名規(guī)范的話,key也是一個(gè)很好的注釋?zhuān)瑢?duì)象比分支更易讀易擴(kuò)展代碼量更少回憶一下開(kāi)發(fā)-封閉的設(shè)計(jì)原則:對(duì)擴(kuò)展開(kāi)放,對(duì)修改封閉,把容易改變的地方分離出來(lái);策略對(duì)象就是被分離出來(lái)的容易被改變的地方
demo: 從上海出發(fā)到倫敦
//?bad
if?(money?===?100)?{
??way?=?train()
}?else?if?(money?===?200)?{
??way?=?car()
}?else?if?(money?===?300)?{
??way?=?ship()
}?else?if?(money?===?400)?{
??way?=?flight()
}
//?good
const?wayMap?=?{
??m100:?train(),
??m200:?car(),
??m300:?ship(),
??m400:?flight()
}
【應(yīng)用】
表單校驗(yàn)、算法分類(lèi)、其他選擇性較多且目標(biāo)相同的需求
【優(yōu)點(diǎn)】
避免多重條件選擇語(yǔ)句 遵循開(kāi)發(fā)封閉原則,代碼更易閱讀、維護(hù)和擴(kuò)展,一定程度上減少代碼數(shù)量 提高可復(fù)用性
【缺點(diǎn)】
違背最少知識(shí)原則,使用策略對(duì)象處耦合性大

4. 狀態(tài)模式
【定義】
狀態(tài)模式的關(guān)鍵是區(qū)分事物內(nèi)部的狀態(tài),事物內(nèi)部狀態(tài)的改變往往會(huì)帶來(lái)事物的行為改變
【理解】
狀態(tài)模式跟策略模式在實(shí)現(xiàn)上有一定的相似之處,但是目的不一樣,策略模式每條策略都是平行平等的,而狀態(tài)模式最大的區(qū)別是所有狀態(tài)都是預(yù)先定義好的煮個(gè)栗子?:電風(fēng)扇的1-5擋是狀態(tài)模式,1-5擋是預(yù)先定義好的,結(jié)果是風(fēng)力大小不一樣,從上海到倫敦旅游,選擇的交通方式是策略模式,坐汽車(chē)、火車(chē)、飛機(jī)、船是平行的,結(jié)果是你到了倫敦改變行為發(fā)生在狀態(tài)模式內(nèi)部,而策略模式是外部選擇控制的,用戶(hù)也不需要了解狀態(tài)模式的內(nèi)部細(xì)節(jié) 也就是說(shuō)狀態(tài)模式是遵循最少知識(shí)原則的
【應(yīng)用】狀態(tài)機(jī),把狀態(tài)抽離出來(lái)單獨(dú)封裝在一個(gè)對(duì)象里面,直觀方便,只需call(狀態(tài)機(jī))一下使用
demo: 每個(gè)年齡段都要去做相對(duì)應(yīng)的事
//?bad
if?(age?===?7)?{
??work?=?primarySchool()
}?else?if?(age?===?13)?{
??work?=?juniorMiddleSchool()
}?else?if?(age?===?16)?{
??work?=?highSchool()
}?else?if?(age?===?19)?{
??work?=?university()
}?else?if?(age?===?23)?{
??work?=?growUP()
}?else?if?(age?>?24)?{
??work?=?makeMoney()
}
//?good
const?workMap?=?{
??age7:?primarySchool(),
??age13:?juniorMiddleSchool(),
??age16:?highSchool(),
??age19:?university(),
??age23:?growUP(),
??ageMore:?makeMoney(),
}
【優(yōu)點(diǎn)】容易增加新?tīng)顟B(tài)或者切換當(dāng)前狀態(tài) 請(qǐng)求動(dòng)作跟狀態(tài)中封裝的行為解耦 易維護(hù)擴(kuò)展
【缺點(diǎn)】
邏輯分散不易讀,狀態(tài)多比較枯燥(if else更枯燥?)
5. 享元模式
【定義】
享元(flyweight)模式是一種用于性能優(yōu)化的模式享元模式的核心是運(yùn)用共享技術(shù)來(lái)有效支持大量細(xì)粒度的對(duì)象
【理解】
為什么說(shuō)是一種性能優(yōu)化模式?很多人喜歡在for循環(huán)里new一個(gè)對(duì)象,然后利用循環(huán)對(duì)象的當(dāng)前值去改變這個(gè)對(duì)象再去寫(xiě)一些邏輯,其實(shí)很多時(shí)候這些對(duì)象改變之后還是有很多的相似之處的,享元模式就是把這些在循環(huán)里創(chuàng)建的對(duì)象提取到循環(huán)外層創(chuàng)建一次,然后循環(huán)內(nèi)每次做一定改動(dòng)再去寫(xiě)xx邏輯(這里只是舉例循環(huán),享元模式可以在很多地方使用不限于循環(huán)) 為了幫助理解(其實(shí)我覺(jué)得這個(gè)模式很好理解),書(shū)上舉例說(shuō):有100種衣服,讓100個(gè)模特一人試一件 和 一個(gè)模特試100件,假如請(qǐng)一個(gè)模特是100塊(創(chuàng)建對(duì)象消耗),你愿意請(qǐng)多少個(gè)模特,一個(gè)模特試100件衣服肯定比100個(gè)模特慢,所以,享元模式是一種利用時(shí)間換取空間的優(yōu)化模式
一個(gè)測(cè)試:像let const 這種塊級(jí)作用域,代碼塊執(zhí)行完會(huì)立即釋放變量?jī)?nèi)存的


let?aaa?=?()?=>?{
??debugger
??for?(let?i?=?0;?i?4;?i++)?{
????let?x?=?new?Object({name:?i})
????console.log(x)
??}
}
aaa()?//?{name:?0}?{name:?1}?{name:?2}?{name:?3}
let?aaa?=?()?=>?{
??debugger
??for?(let?i?=?0;?i?4;?i++)?{
????let?x?=?{name:?i}
????console.log(x)
??}
}
aaa()?//?{name:?3}?{name:?3}?{name:?3}?{name:?3}


這兩種其實(shí)是一樣,除了對(duì)象創(chuàng)建的方式不同,其他一樣,對(duì)比是讓你明白,第一種方式也會(huì)創(chuàng)建一個(gè)新的對(duì)象,x指針的指向是新創(chuàng)建的對(duì)象的引用地址
【應(yīng)用】
如果考慮需要復(fù)用元對(duì)象的話,建議定義好元對(duì)象,然后使用的地方深拷貝或者淺拷貝一下再使用,這樣其他地方也可以使用而不破壞元對(duì)象啦,注意JSON copy的坑
let?obj?=?{
??name:?'xx',
??age:?0,
??aaa:?111,
??bbb:?222,
??ccc:?333
}
let?a?=?Object.assgin({},?obj)
//?let?a?=?JSON.parse(JSON.stringify(obj))
let?aaa?=?key?=>?{
??console.log(key.age)
}
for?(let?i?=?1;?i?5;?i++)?{
??a.age?=?i
??aaa(a)
}?
其實(shí)我覺(jué)得享元模式跟單例模式有一定的相似之處的,雖然書(shū)上沒(méi)提,但我真覺(jué)得這倆模式真的很像,單例模式被copy一下就是享元模式了,但是單例還是有特殊之處的,所有的狀態(tài)屬性方法都?xì)w于一個(gè)對(duì)象or實(shí)例上,并且提供全局的訪問(wèn)接口,想想都很強(qiáng)大,但是也比較難以維護(hù)
適用性:

【優(yōu)點(diǎn)】都說(shuō)了是一種性能優(yōu)化模式了,那有點(diǎn)肯定就是性能優(yōu)化了,解決大量對(duì)象帶來(lái)的性能問(wèn)題【缺點(diǎn)】缺點(diǎn)就是你要花時(shí)間學(xué)習(xí)并理解它 ?--哈撒給

6. 職責(zé)鏈模式模式
【定義】
使多個(gè)對(duì)象都有機(jī)會(huì)處理請(qǐng)求,從而避免請(qǐng)求的發(fā)送者和接收者之間的耦合關(guān)系,將這些對(duì)象連成一條鏈,并沿著這條鏈傳遞該請(qǐng)求,直到有一個(gè)對(duì)象處理它為止。
【理解】
打個(gè)比方,有個(gè)魔方,100個(gè)人里面有10個(gè)人會(huì)解這個(gè)魔方,現(xiàn)在魔方在你手上,你不知道誰(shuí)會(huì)解魔方,但是你知道某幾個(gè)人可能會(huì)解魔方,于是你把魔方傳給A,A不會(huì)再傳給B(被傳的都是已知有可能會(huì)解魔方的人)直到有人解開(kāi)魔方某些場(chǎng)景下你需要費(fèi)很大力氣才能建立請(qǐng)求者和接收者之間的聯(lián)系,這樣使兩者直接需要很大的耦合性,而使用職責(zé)鏈模式可以解除這種耦合作用域、原型鏈、事件冒泡跟職責(zé)鏈?zhǔn)遣皇呛芟嗨?,個(gè)人覺(jué)得跟promise也有點(diǎn)相似,從接口獲取到數(shù)據(jù)然后一層一層的promise去篩選過(guò)濾最后達(dá)到業(yè)務(wù)層,業(yè)務(wù)層拿到數(shù)據(jù)處理業(yè)務(wù)邏輯or呈現(xiàn)數(shù)據(jù)
【應(yīng)用】
管理代碼、其他類(lèi)似事件冒泡的需求,說(shuō)實(shí)話 我?guī)缀鯖](méi)用過(guò)這個(gè)模式
【優(yōu)點(diǎn)】
降低發(fā)起請(qǐng)求的對(duì)象和處理請(qǐng)求的對(duì)象之間的耦合性
【缺點(diǎn)】
鏈條太長(zhǎng)對(duì)性能有較大影響
7. 裝飾者模式
【定義】
裝飾者模式可以動(dòng)態(tài)地給某個(gè)對(duì)象添加一些額外的職責(zé)裝飾者模式能夠在不改變對(duì)象自身的基礎(chǔ)上,在程序運(yùn)行期間給對(duì)象動(dòng)態(tài)地添加職責(zé)
【理解】
鋼鐵俠撿到了滅霸的手套?并打了個(gè)響指
【應(yīng)用】demo:
var?plane?=?{
fire:?function(){
console.log(?'發(fā)射普通子彈'?);?}
}
var?missileDecorator?=?function(){?console.log(?'發(fā)射導(dǎo)彈'?);
}
var?atomDecorator?=?function(){?console.log(?'發(fā)射原子彈'?);
}
var?fire1?=?plane.fire;
plane.fire?=?function(){?fire1();
missileDecorator();?}
var?fire2?=?plane.fire;
plane.fire?=?function(){?fire2();
atomDecorator();?}
plane.fire();
//?分別輸出:?發(fā)射普通子彈、發(fā)射導(dǎo)彈、發(fā)射原子彈
使用AOP裝飾函數(shù)


把這段看懂就ok了

【優(yōu)點(diǎn)】擴(kuò)展性好、隨變化而變化,靈活、甚至預(yù)測(cè)需求
【缺點(diǎn)】暫時(shí)沒(méi)想到...

8. 發(fā)布-訂閱模式模式
【定義】
發(fā)布—訂閱模式(其實(shí)也有一種叫觀察者模式的模式,跟這個(gè)很像,但是沒(méi)有中間的調(diào)度層),它定義對(duì)象間的一種一對(duì)多的依賴(lài)關(guān)系,當(dāng)一個(gè)對(duì)象的狀 態(tài)發(fā)生改變時(shí),所有依賴(lài)于它的對(duì)象都將得到通知。在 JavaScript 開(kāi)發(fā)中,我們一般用事件模型 來(lái)替代傳統(tǒng)的發(fā)布—訂閱模式
【理解】
Vue的Bus通信就是一種發(fā)布-訂閱模式,中間的vue實(shí)例就是調(diào)度層,但是這個(gè)必須得先監(jiān)聽(tīng)再推送,推送過(guò)的再添加監(jiān)聽(tīng)就監(jiān)聽(tīng)不到了而觀察者模式指的是觀察者與被觀察者之間直接的通訊

【應(yīng)用】
發(fā)布者的每次發(fā)布可以緩存起來(lái),等有新的訂閱者時(shí),再把之前緩存的消息一起發(fā)送給新的訂閱者,當(dāng)然這只是有這個(gè)需求才這么做,沒(méi)有的話就不用緩存了 現(xiàn)在流行的MVVM框架源碼都有用到發(fā)布-訂閱模式 addEventListener,添加事件監(jiān)聽(tīng)
【優(yōu)點(diǎn)】可以隨意增加或刪除訂閱者,這不會(huì)影響到發(fā)布者的代碼編寫(xiě),耦合低
【缺點(diǎn)】消耗時(shí)間、內(nèi)存、嵌套過(guò)深導(dǎo)致難以追蹤
四、編程技巧
1. 提煉函數(shù)
單一職責(zé)原則 如果一個(gè)函數(shù)過(guò)長(zhǎng),不得不通過(guò)若干注釋來(lái)顯得易讀,說(shuō)明這個(gè)函數(shù)需要分解 好處:避免超大函數(shù),代碼復(fù)用,利用修改或覆蓋,良好的函數(shù)命名也是一種注釋 編碼規(guī)范:理論上函數(shù)不超過(guò)50行,最好不超過(guò)20行
2. 合并重復(fù)的條件片段
定義:條件分支語(yǔ)句存在重復(fù)的時(shí)候,有必要進(jìn)行去重工作 demo:
//?bad
var?paging?=?function(currPage)?{
????if?(currPage?<=?0)?{
????????currPage?=?0
????????jump(currPage)
????}?else?if?(currPage?<=?1)?{
????????currPage?=?1
????????jump(currPage)
????}?else?{
????????jump(currPage)
????}
}
//?good
var?paging?=?function(currPage)?{
????if?(currPage?<=?0)?{
????????currPage?=?0
????}?else?if?(currPage?<=?1)?{
????????currPage?=?1
????}
????jump(currPage)
}
3. 把條件分支語(yǔ)句提煉成函數(shù)
復(fù)雜的條件語(yǔ)句是難讀的 可以把這句代碼提煉成一個(gè)單獨(dú)的函數(shù),既能更準(zhǔn)確地表達(dá)代碼的意思, 函數(shù)名本身又能起到注釋的作用 我加一句,如果需要修改條件的話,這樣寫(xiě)也是更容易維護(hù)的
4. 合理使用循環(huán)
對(duì)于重復(fù)性工作,建議使用循環(huán) 比如:把key都放在一個(gè)數(shù)組里面,然后循環(huán)數(shù)組,new個(gè)key就可以了 有時(shí)候新增修改刪除key等很方便

5. 提前讓函數(shù)退出代替嵌套條件分支
一個(gè)函數(shù)只有一個(gè)入口,但是可以有多個(gè)出口 所以不必把所有的邏輯進(jìn)行完,最后return,如果不關(guān)注剩下的邏輯,在滿(mǎn)足條件的時(shí)候就可以return,這樣做的目的不是程序少執(zhí)行了哪幾步,而是代碼更簡(jiǎn)潔清晰易讀
6. 傳遞對(duì)象參數(shù)代替過(guò)長(zhǎng)的參數(shù)列表(面向?qū)ο缶幊?-編程模式--配置對(duì)象)
這個(gè)在面向?qū)ο缶幊痰木幊棠J秸鹿?jié)有說(shuō),是編程模式的一個(gè)case,叫做配置對(duì)象,也就是把若干個(gè)參數(shù)放到到一個(gè)對(duì)象里面,成了一個(gè)集合,這個(gè)對(duì)象可以叫做配置對(duì)象
不用考慮參數(shù)的順序 可以跳過(guò)某些參數(shù)的設(shè)置 函數(shù)擴(kuò)展性更強(qiáng),可以適應(yīng)將來(lái)的擴(kuò)展需要 代碼可讀性更好,因?yàn)樵诖a中我們看到的是配置對(duì)象的屬性名稱(chēng)
7. 盡量減少參數(shù)數(shù)量
一句話:不要傳沒(méi)必要的參數(shù),如果是備著以后擴(kuò)展的話,等到擴(kuò)展的時(shí)候再加也不遲
8. 少用三目運(yùn)算符(三元運(yùn)算符)
有一些程序員喜歡大規(guī)模地使用三目運(yùn)算符,來(lái)代替?zhèn)鹘y(tǒng)的 if、else。理由是三目運(yùn)算符性 能高,代碼量少。不過(guò),這兩個(gè)理由其實(shí)都很難站得住腳(這是書(shū)上原話)
關(guān)于簡(jiǎn)潔性,三元運(yùn)算符絕對(duì)比if else簡(jiǎn)潔,但是要分場(chǎng)合使用,條件分支邏輯簡(jiǎn)單的時(shí)候建議用三目運(yùn)算符,但是條件分支邏輯復(fù)雜的時(shí)候,易讀跟維護(hù)性都是很差的,比如:

對(duì)于性能,我做了個(gè)簡(jiǎn)單的測(cè)試,如圖,0.2ms 除以 百萬(wàn) 是可以忽略不計(jì)的,所以性能上幾乎沒(méi)差的


9. 合理使用鏈?zhǔn)秸{(diào)用
書(shū)上說(shuō)的鏈?zhǔn)秸{(diào)用(類(lèi)似這個(gè):console.log( new User().setId( 1314 ).setName( 'sven' ) );)節(jié)約的字節(jié)數(shù)量微不足道,鏈?zhǔn)秸{(diào)用的壞處就在不好調(diào)試上,我看chrome上一般都把錯(cuò)誤一層一層的細(xì)化,然后我測(cè)試了一下這種鏈?zhǔn)秸{(diào)用會(huì)不會(huì)具體到其中一個(gè)方法,結(jié)果是不會(huì),如圖;(以前編碼肯定也遇到過(guò)鏈?zhǔn)秸{(diào)用報(bào)錯(cuò),但是沒(méi)注意到這個(gè)細(xì)節(jié),作者連調(diào)試難度都考慮到了,在下跪拜...)

如果該鏈條的結(jié)構(gòu)相對(duì)穩(wěn)定,后期不易發(fā)生修改,那么使用鏈?zhǔn)秸{(diào)用無(wú)可厚非。但如果該鏈 條很容易發(fā)生變化,導(dǎo)致調(diào)試和維護(hù)困難,那么還是建議使用普通調(diào)用的形式 一句話總結(jié):穩(wěn)定的鏈條可以寫(xiě),不穩(wěn)定的不要寫(xiě)
10. 分解大型類(lèi)
這個(gè)我不太想贅述,感覺(jué)很多的原則or模式都有點(diǎn)似曾相識(shí)的感覺(jué),這個(gè)一看不還是單一職責(zé)原則,大拆小,小的組合成大的,到底還是為了單個(gè)小功能的可讀可維護(hù)擴(kuò)展等等更好
11. 用return退出多重循環(huán)
假設(shè)在函數(shù)體內(nèi)有一個(gè)兩重循環(huán)語(yǔ)句,我們需要在內(nèi)層循環(huán)中判斷,當(dāng)達(dá)到某個(gè)臨界條件時(shí)退出外層的循環(huán)。我們大多數(shù)時(shí)候會(huì)引入一個(gè)控制標(biāo)記變量 這玩意我以前好像就是這么處理的,引入一個(gè)標(biāo)志位,外層循環(huán)每次都判斷這個(gè)標(biāo)志位,符合條件就結(jié)束循環(huán)

11. 結(jié)束語(yǔ)
整篇寫(xiě)完自己對(duì)設(shè)計(jì)原則和部分設(shè)計(jì)模式有了更深的了解,文中有不當(dāng)之處歡迎評(píng)論區(qū)指正, 寫(xiě)完我有個(gè)反思,有的時(shí)候?qū)憱|西喜歡叨叨,生怕別人沒(méi)看到?jīng)]看懂,以后一定精簡(jiǎn)

掃碼關(guān)注公眾號(hào),訂閱更多精彩內(nèi)容。
