我理解的前端設(shè)計(jì)模式
點(diǎn)擊上方 前端瓶子君,關(guān)注公眾號(hào)
回復(fù)算法,加入前端編程面試算法每日一題群

來源:hannie76327
https://juejin.cn/post/6953872475537014820
用自己通俗易懂的語言理解設(shè)計(jì)模式。
通過對(duì)每種設(shè)計(jì)模式的學(xué)習(xí),更加加深了我對(duì)它的理解,也能在工作中考慮應(yīng)用場(chǎng)合。
成文思路:分析每種設(shè)計(jì)模式思想、抽離出應(yīng)用場(chǎng)景、對(duì)這些模式進(jìn)行對(duì)比
此篇文章包含:修飾者模式(裝飾器)、單例模式、工廠模式、訂閱者模式、觀察者模式、代理模式
將不變的部分和變化的部分隔開是每個(gè)設(shè)計(jì)模式的主題。
單例模式
也叫單體模式,核心思想是確保一個(gè)類只對(duì)應(yīng)一個(gè)實(shí)例。
特點(diǎn):
只允許一個(gè)例存在,提供全局訪問點(diǎn),緩存初次創(chuàng)建的變量對(duì)象
排除全局變量,防止全局變量被重寫
可全局訪問
// 工廠模式和new模式實(shí)現(xiàn)單例模式
vue的安裝插件屬于單例模式
適用場(chǎng)景:適用于彈框的實(shí)現(xiàn), 全局緩存,一個(gè)單一對(duì)象。比如:彈窗,無論點(diǎn)擊多少次,彈窗只應(yīng)該被創(chuàng)建一次。
缺點(diǎn):
防止全局變量被污染,看過多種寫法,總結(jié)在一起,更能融會(huì)貫通
直接使用字面量(全局對(duì)象)
const person = {
name: '哈哈',
age: 18
}
了解 const 語法的小伙伴都知道,這只喵是不能被重新賦值的,但是它里面的屬性其實(shí)是可變的。
如果想要一個(gè)不可變的單例對(duì)象:
const person = {
name: '哈哈',
age: 18
}
Object.freeze(person);
這樣就不能新增或修改person的任何屬性.
如果是在模塊中使用,上面的寫法并不會(huì)污染全局作用域,但是直接生成一個(gè)固定的對(duì)象缺少了一些靈活性。
使用構(gòu)造函數(shù)的靜態(tài)屬性
class寫法
class A {
constructor () {
if (!A._singleton) {
A._singleton = this;
}
return A._singleton;
}
log (...args) {
console.log(...args);
}
}
var a1 = new A()
var a2= new A()
console.log(a1 === a2)//true
構(gòu)造函數(shù)寫法
function A(name){
// 如果已存在對(duì)應(yīng)的實(shí)例
if(typeof A._singleton === 'object'){
return A._singleton
}
//否則正常創(chuàng)建實(shí)例
this.name = name
// 緩存
A._singleton =this
return this
}
var a1 = new A()
var a2= new A()
console.log(a1 === a2)//true
缺點(diǎn):在于靜態(tài)屬性是能夠被人為重寫的,不過不會(huì)像全局變量那樣被無意修改。
借助閉包
考慮重寫構(gòu)造函數(shù):當(dāng)對(duì)象第一次被創(chuàng)建以后,重寫構(gòu)造函數(shù),在重寫后的構(gòu)造函數(shù)里面訪問私有變量。
function A(name){
var instance = this
this.name = name
//重寫構(gòu)造函數(shù)
A = function (){
return instance
}
//重寫構(gòu)造函數(shù)之后,實(shí)際上原先的A指針對(duì)應(yīng)的函數(shù)實(shí)際上還在內(nèi)存中(因?yàn)閕nstance變量還在被引用著),但是此時(shí)A指針已經(jīng)指向了一個(gè)新的函數(shù)了
}
A.prototype.pro1 = "from protptype1"
var a1 = new A()
A.prototype.pro2 = "from protptype2"
var a2= new A()
console.log(a1.pro1)//from protptype1
console.log(a1.pro2)//underfined
console.log(a2.pro1)//from protptype1
console.log(a2.pro2)//underfined
console.log(a1.constructor ==== A) //false
為了解決A指針指向新地址的問題,實(shí)現(xiàn)原型鏈繼承
function A(name){
var instance = this
this.name = name
//重寫構(gòu)造函數(shù)
A = function (){
return instance
}
// 第一種寫法,這里實(shí)際上實(shí)現(xiàn)了一次原型鏈繼承,如果不想這樣實(shí)現(xiàn),也可以直接指向舊的原型
A.prototype = this
// 第二種寫法,直接指向舊的原型
A.prototype = this.constructor.prototype
instance = new A()
// 調(diào)整構(gòu)造函數(shù)指針,這里實(shí)際上實(shí)現(xiàn)了一次原型鏈繼承,如果不想這樣實(shí)現(xiàn),也可以直接指向原來的原型
instance.constructor = A
return instance
}
A.prototype.pro1 = "from protptype1"
var a1 = new A()
A.prototype.pro2 = "from protptype2"
var a2= new A()
console.log(a1.pro1)//from protptype1
console.log(a1.pro2)//from protptype2
console.log(a2.pro1)//from protptype1
console.log(a2.pro2)//from protptype2
利用立即執(zhí)行函數(shù)來保持私有變量
var A;
(function(name){
var instance;
A = function(name){
if(instance){
return instance
}
//賦值給私有變量
instance = this
//自身屬性
this.name = name
}
}());
A.prototype.pro1 = "from protptype1"
var a1 = new A('a1')
A.prototype.pro2 = "from protptype2"
var a2 = new A('a2')
console.log(a1.name)
console.log(a1.pro1)//from protptype1
console.log(a1.pro2)//from protptype2
console.log(a2.pro1)//from protptype1
console.log(a2.pro2)//from protptype2
以上通過閉包的方式可以實(shí)現(xiàn)單例
代理實(shí)現(xiàn)單例模式
function singleton(name){
this.name = name
}
let proxySingleton = function(){
let instance = null
return function(name){
if(!instance){
instance = new singleton(name)
}
return instance
}
}()
let a1= new proxySingleton('a1')
let a2= new proxySingleton('a2')
console.log(123, a1===a2)
工廠單例
let logger = null
class Logger {
log (...args) {
console.log(...args);
}
}
function createLogger() {
if (!logger) {
logger = new Logger();
}
return logger;
}
let a = new createLogger().log('12')
let b = new createLogger().log('121')
console.log(new createLogger(), a===b)
根據(jù)理解,我自己喜歡用代理方式實(shí)現(xiàn),更好理解。如果總結(jié)有錯(cuò),歡迎指正。
參考:?jiǎn)卫J?/p>
工廠模式
不暴露創(chuàng)建對(duì)象的邏輯,封裝在一個(gè)函數(shù)中。工廠模式根據(jù)抽象程度的不同可以分為:簡(jiǎn)單工廠,工廠方法和抽象工廠。
簡(jiǎn)單工廠模式
簡(jiǎn)單工廠模式又叫靜態(tài)工廠模式,由一個(gè)工廠對(duì)象決定創(chuàng)建某一種產(chǎn)品對(duì)象類的實(shí)例。主要用來創(chuàng)建同一類對(duì)象。
簡(jiǎn)單工廠的優(yōu)點(diǎn)在于,你只需要一個(gè)正確的參數(shù),就可以獲取到你所需要的對(duì)象,而無需知道其創(chuàng)建的具體細(xì)節(jié)。
但是在函數(shù)內(nèi)包含了所有對(duì)象的創(chuàng)建邏輯(構(gòu)造函數(shù))和判斷邏輯的代碼,每增加新的構(gòu)造函數(shù)還需要修改判斷邏輯代碼。當(dāng)我們的對(duì)象不是上面的3個(gè)而是30個(gè)或更多時(shí),這個(gè)函數(shù)會(huì)成為一個(gè)龐大的超級(jí)函數(shù),便得難以維護(hù)。所以,簡(jiǎn)單工廠只能作用于創(chuàng)建的對(duì)象數(shù)量較少,對(duì)象的創(chuàng)建邏輯不復(fù)雜時(shí)使用。
工廠方法模式
工廠方法模式的本意是將實(shí)際創(chuàng)建對(duì)象的工作推遲到子類中,這樣核心類就變成了抽象類。
在工廠方法模式中,工廠父類負(fù)責(zé)定義創(chuàng)建產(chǎn)品對(duì)象的公共接口,而工廠子類則負(fù)責(zé)生成具體的產(chǎn)品對(duì)象, 這樣做的目的是將產(chǎn)品類的實(shí)例化操作延遲到工廠子類中完成,即通過工廠子類來確定究竟應(yīng)該實(shí)例化哪一個(gè)具體產(chǎn)品類。
抽象工廠模式
抽象工廠其實(shí)是實(shí)現(xiàn)子類繼承父類的方法。
抽象工廠模式(Abstract Factory Pattern),提供一個(gè)創(chuàng)建一系列相關(guān)或相互依賴對(duì)象的接口,而無須指定它們具體的類。
抽象工廠可以提供多個(gè)產(chǎn)品對(duì)象,而不是單一的產(chǎn)品對(duì)象。
參考 :JavaScript設(shè)計(jì)模式與實(shí)踐--工廠模式
觀察者模式或發(fā)布訂閱模式
通常又被稱為 發(fā)布-訂閱者模式 或 消息機(jī)制,它**定義了對(duì)象間的一種一對(duì)多的依賴關(guān)系**,只要當(dāng)一個(gè)對(duì)象的狀態(tài)發(fā)生改變時(shí),所有依賴于它的對(duì)象都得到通知并被自動(dòng)更新,解決了主體對(duì)象與觀察者之間功能的耦合,即一個(gè)對(duì)象狀態(tài)改變給其他對(duì)象通知的問題。
最好理解的舉例:公司里發(fā)布通知,讓員工都知道。
工作中碰到以下幾種,并進(jìn)行分析。
用雙向綁定來分析此模式:
雙向綁定維護(hù)4個(gè)模塊:observer監(jiān)聽者、dep訂閱器、watcher訂閱者、compile編譯者
訂閱器是手機(jī)訂閱者(依賴),如果屬性發(fā)生變化observer通知dep,dep通知watcher調(diào)用update函數(shù)(watcher類中有update函數(shù),并且將自己加入dep)去更新數(shù)據(jù),這是符合一對(duì)多的思想,也就是observer是一,watcher是多。compile解析指令,訂閱數(shù)據(jù)變化,綁定更新函數(shù)。
理解下來,compile類似于綁定員工的角色,把watcher加入一個(gè)集體,observer通知它們執(zhí)行。
用子組件與父組件通信分析此模式:
通過 emit來發(fā)布消息,對(duì)訂閱者emit 來發(fā)布消息,對(duì)訂閱者 emit來發(fā)布消息,對(duì)訂閱者on 做統(tǒng)一處理。
emit是發(fā)布訂閱者,emit是發(fā)布訂閱者,emit是發(fā)布訂閱者,on 是監(jiān)聽執(zhí)行
用DOM的事件綁定(比如click)分析此模式:
addEventListener('click',()=>{})監(jiān)聽click事件,當(dāng)點(diǎn)擊DOM就是向訂閱者發(fā)布這個(gè)消息。
點(diǎn)擊DOM是發(fā)布,addEventListener是監(jiān)聽執(zhí)行
小結(jié)
通過分析平時(shí)碰到的這種模式,更好的理解一和多分別對(duì)應(yīng)什么,也增加記憶。
一(發(fā)布事件):通知、廣播發(fā)布
多(訂閱事件,可能會(huì)做出不同的回應(yīng)):觀察者、監(jiān)聽者、訂閱者
發(fā)布和訂閱的意思都是變成多的角度(添加)
一是對(duì)應(yīng)執(zhí)行,多是收集
平時(shí)碰到的函數(shù)理解,寫自己的函數(shù)也可以這么定義,有個(gè)全局Events=[]:
publish/emit/點(diǎn)擊/notify 訂閱事件 調(diào)用函數(shù) subscribe/on/addEventListener 監(jiān)聽 push函數(shù)
unsubscribe/off/removeEventListener 刪除
其實(shí)分析以上幾種情況,發(fā)布訂閱模式和觀察者模式的思想差不多相同,但是也是有區(qū)別:
觀察者模式中需要觀察者對(duì)象自己定義事件發(fā)生時(shí)的相應(yīng)方法。 發(fā)布訂閱模式者在發(fā)布對(duì)象和訂閱對(duì)象之中加了一個(gè)中介對(duì)象。我們不需要在乎發(fā)布者對(duì)象和訂閱者對(duì)象的內(nèi)部是什么,具體響應(yīng)時(shí)間細(xì)節(jié)全部由中介對(duì)象實(shí)現(xiàn)。
訂閱的東西用Map或者Object類型來存儲(chǔ)。
發(fā)布訂閱模式,有個(gè)中介,也可以說是channel,但發(fā)現(xiàn)代碼實(shí)現(xiàn)差不多,只不過發(fā)布訂閱用來寫包含有回調(diào)函數(shù)
實(shí)例參考:JavaScript 設(shè)計(jì)模式之觀察者模式與發(fā)布訂閱模式
juejin.cn/post/684490…
juejin.cn/post/684490…
裝飾者模式
裝飾者模式的定義:在不改變對(duì)象自身的基礎(chǔ)上,在程序運(yùn)行期間給對(duì)象動(dòng)態(tài)地添加方法。
裝飾者模式的適用場(chǎng)合:
如果你需要為類增添特性或職責(zé),可是從類派生子類的解決方法并不太現(xiàn)實(shí)的情況下,就應(yīng)該使用裝飾者模式。 如果想為對(duì)象增添特性又不想改變使用該對(duì)象的代碼的話,則可以采用裝飾者模式。 原有方法維持不變,在原有方法上再掛載其他方法來滿足現(xiàn)有需求;函數(shù)的解耦,將函數(shù)拆分成多個(gè)可復(fù)用的函數(shù),再將拆分出來的函數(shù)掛載到某個(gè)函數(shù)上,實(shí)現(xiàn)相同的效果但增強(qiáng)了復(fù)用性。
裝飾者模式除了可以應(yīng)用在類上之外,還可以應(yīng)用在函數(shù)上(其實(shí)這就是高階函數(shù))
我覺得可以是函數(shù)封裝原函數(shù)。這樣不改變?cè)瓉?/p>
舉例:為汽車添加反光燈、后視鏡等這些配件
碰到的:對(duì)函數(shù)進(jìn)行增強(qiáng)(節(jié)流函數(shù)or防抖函數(shù)、緩存函數(shù)返回值、構(gòu)造React高階組件,為組件增加額外的功能)
參考:使用裝飾者模式做有趣的事情
代理模式
所謂的的代理模式就是為一個(gè)對(duì)象找一個(gè)替代對(duì)象,以便對(duì)原對(duì)象進(jìn)行訪問。
使用代理的原因是我們不愿意或者不想對(duì)原對(duì)象進(jìn)行直接操作,我們使用代理就是讓它幫原對(duì)象進(jìn)行一系列的操作,等這些東西做完后告訴原對(duì)象就行了。就像我們生活的那些明星的助理經(jīng)紀(jì)人一樣。
原則:?jiǎn)我辉瓌t
常用的虛代理形式:保護(hù)代理、緩存代理、虛擬代理。
保護(hù)代理:明星委托助理或者經(jīng)紀(jì)人所要干的事;
緩存代理:緩存代理就是將代理加緩存,更方便單一原則;
常用的虛擬代理:某一個(gè)花銷很大的操作,可以通過虛擬代理的方式延遲到這種需要它的時(shí)候才去創(chuàng)建(例:使用虛擬代理實(shí)現(xiàn)圖片懶加載);
先占位,加載完,再加載所需圖片
var imgFunc = (function() {
var imgNode = document.createElement('img');
document.body.appendChild(imgNode);
return {
setSrc: function(src) {
imgNode.src = src;
}
}
})();
var proxyImage = (function() {
var img = new Image();
img.onload = function() {
imgFunc.setSrc(this.src);
}
return {
setSrc: function(src) {
imgFunc.setSrc('./loading,gif');
img.src = src;
}
}
})();
proxyImage.setSrc('./pic.png');
復(fù)制代碼
碰到的:Vue的Proxy、懶加載圖片加占位符、冒泡點(diǎn)擊DOM元素
參考:javascript 代理模式(通俗易懂)
策略模式
策略模式的定義:定義一系列的算法,把他們一個(gè)個(gè)封裝起來,并且使他們可以相互替換。
策略模式的重心不是如何實(shí)現(xiàn)算法,而是如何組織、調(diào)用這些算法,從而讓程序結(jié)構(gòu)更靈活、可維護(hù)、可擴(kuò)展。
策略模式的目的:將算法的使用算法的實(shí)現(xiàn)分離開來。
一個(gè)基于策略模式的程序至少由兩部分組成:
第一個(gè)部分是一組策略類(可變),策略類封裝了具體的算法,并負(fù)責(zé)具體的計(jì)算過程。 第二個(gè)部分是環(huán)境類Context(不變),Context接受客戶的請(qǐng)求,隨后將請(qǐng)求委托給某一個(gè)策略類。要做到這一點(diǎn),說明Context中要維持對(duì)某個(gè)策略對(duì)象的引用。
原則:開放-封閉原則
/*策略類 A B C就是可以替換使用的算法*/
var levelOBJ = {
"A": function(money) {
return money * 4;
},
"B" : function(money) {
return money * 3;
},
"C" : function(money) {
return money * 2;
}
};
/*環(huán)境類,維持對(duì)levelOBJ策略對(duì)象的引用,擁有執(zhí)行算法的能力*/
var calculateBouns =function(level,money) {
return levelOBJ[level](money);
};
console.log(calculateBouns('A',10000)); // 40000
復(fù)制代碼
Context函數(shù)傳入實(shí)際值,調(diào)用策略,可能同時(shí)調(diào)用多個(gè)策略,這樣可以封裝一函數(shù)循環(huán)調(diào)用策略,然后用Context函數(shù)調(diào)用此封裝的函數(shù)
在工作中,很多if else,每種條件執(zhí)行不同的算法,其實(shí)可以用到策略模式,比如驗(yàn)證表單
比如:
多種不同登錄方式(賬號(hào)密碼登錄、手機(jī)驗(yàn)證碼登錄和第三方登錄)。為了方便維護(hù)不同的登錄方式,可以把不同的登錄方式封裝成不同的登錄策略。
驗(yàn)證表單
不同的人發(fā)不同的工資
工作中碰到選擇不同下拉框執(zhí)行不同函數(shù)(策略)
參考:js設(shè)計(jì)模式--策略模式
建造者模式
應(yīng)用場(chǎng)景:
創(chuàng)建時(shí)有很多必填參數(shù)需要驗(yàn)證。 創(chuàng)建時(shí)參數(shù)求值有先后順序、相互依賴。 創(chuàng)建有很多步驟,全部成功才能創(chuàng)建對(duì)象。
class Programmer {
age: number
username: string
color: string
area: string
constructor(p) {
this.age = p.age
this.username = p.username
this.color = p.color
this.area = p.area
}
toString() {
console.log(this)
}
}
class Builder {
age: number
username: string
color: string
area: string
build() {
if (this.age && this.username && this.color && this.area) {
return new Programmer(this)
} else {
throw new Error('缺少信息')
}
}
setAge(age: number) {
if (age > 18 && age < 36) {
this.age = age
return this
} else {
throw new Error('年齡不合適')
}
}
setUsername(username: string) {
if (username !== '小明') {
this.username = username
return this
} else {
throw new Error('小明不合適')
}
}
setColor(color: string) {
if (color !== 'yellow') {
this.color = color
return this
} else {
throw new Error('yellow不合適')
}
}
setArea(area: string) {
this.area = area
return this
}
}
// test
const p = new Builder()
.setAge(20)
.setUsername('小紅')
.setColor('red')
.setArea('hz')
.build()
.toString()
適配模式
舉例:Target Adaptee Adapter ,Adapter是需要繼承Target,并在里面調(diào)用Adaptee中的方法。
形象比擬:Target是目標(biāo)抽象類,實(shí)現(xiàn)插入插口的功能;Adaptee是新的插頭,包含了實(shí)現(xiàn)目標(biāo)的方法;Adapter是implements Target,為了調(diào)用Target方法。這樣,既能保留原功能(原函數(shù)不變),又能執(zhí)行新功能(添加Adaptee Adapter)
Target是要實(shí)現(xiàn)的目標(biāo)(比如打印日志,這是抽象的方法),如果不用適配模式,就需要重寫函數(shù),找到辦法。
Adaptee是適配者類,也就是插口,在適配器Adapter中,implements來自于的Target方法(就是新方法,適配的方法,達(dá)到)中調(diào)用Adaptee中的方法。
Adaptee在Adapter中調(diào)用,Adapter最終是要調(diào)用Adaptee 中需要的具體方法(也就是我們最終要達(dá)到目的使用的方法,此方法可在Target中的抽象方法中實(shí)現(xiàn))。
場(chǎng)景:
1.以前開發(fā)的接口不滿足需求,比如輸出log存在本地盤改成存入云盤
2.使用第三方提供的組件,但組件接口定義和自己要求的接口定義不同
面試過程中,定義Target Adapter Adaptee分別實(shí)現(xiàn)的功能,再套用原理來實(shí)現(xiàn)。
用ts實(shí)現(xiàn),這樣能使用interface和implements,更符合面向?qū)ο笳Z言
優(yōu)點(diǎn)
將目標(biāo)類和適配者類解耦,通過引入一個(gè)適配器類來重用現(xiàn)有的適配者類,而無須修改原有代碼。
增加了類的透明性和復(fù)用性,將具體的實(shí)現(xiàn)封裝在適配者類中,對(duì)于客戶端類來說是透明的,而且提高了適配者的復(fù)用性。
靈活性和擴(kuò)展性都非常好,通過使用配置文件,可以很方便地更換適配器,也可以
在不修改原有代碼的基礎(chǔ)上增加新的適配器類,符合開閉原則。
缺點(diǎn)
過多地使用適配器,會(huì)讓系統(tǒng)非常零亂,不易整體進(jìn)行把握。
參考:TypeScript 設(shè)計(jì)模式之適配器模式
模板方法模式
很好理解,看它能很快理解
這九種常用的設(shè)計(jì)模式你掌握了嗎
職責(zé)鏈模式
應(yīng)用場(chǎng)景:
多個(gè)處理器 ABC 依次處理同一個(gè)請(qǐng)求,形成一個(gè)鏈條,當(dāng)某個(gè)處理器能處理這個(gè)請(qǐng)求,就不會(huì)繼續(xù)傳遞給后續(xù)處理器了。 過濾器 攔截器 處理器。
const order500 = function (orderType, pay, stock) {
if (orderType === 1 && pay === true) {
console.log("500 元定金預(yù)購, 得到 100 元優(yōu)惠券");
return true;
} else {
return false;
}
};
const order200 = function (orderType, pay, stock) {
if (orderType === 2 && pay === true) {
console.log("200 元定金預(yù)購, 得到 50 元優(yōu)惠券");
return true;
} else {
return false;
}
};
const orderCommon = function (orderType, pay, stock) {
if ((orderType === 3 || !pay) && stock > 0) {
console.log("普通購買, 無優(yōu)惠券");
return true;
} else {
console.log("庫存不夠, 無法購買");
return false;
}
};
class chain {
fn: Function
nextFn: Function
constructor(fn: Function) {
this.fn = fn;
this.nextFn = null;
}
setNext(nextFn) {
this.nextFn = nextFn
}
init(...arguments) {
const result = this.fn(...arguments);
if (!result && this.nextFn) {
this.nextFn.init(...arguments); //這里看不懂
}
}
}
const order500New = new chain(order500);
const order200New = new chain(order200);
const orderCommonNew = new chain(orderCommon);
order500New.setNext(order200New);
order200New.setNext(orderCommonNew);
order500New.init(3, true, 500); // 普通購買, 無優(yōu)惠券
其他參考
juejin.cn/post/684490… JavaScript 中常見設(shè)計(jì)模式整理
juejin.cn/post/695342… *
juejin.cn/post/688138… *
juejin.cn/post/684490…
juejin.cn/post/684490…
