JavaScript 的一些常用設(shè)計模式
設(shè)計模式是前人解決某個特定場景下對而總結(jié)出來的一些解決方案??赡軇傞_始接觸編程還沒有什么經(jīng)驗(yàn)的時候,會感覺設(shè)計模式?jīng)]那么好理解,這個也很正常。有些簡單的設(shè)計模式我們有時候用到,不過沒意識到也是存在的。
學(xué)習(xí)設(shè)計模式,可以讓我們在處理問題的時候提供更多更快的解決思路。
當(dāng)然設(shè)計模式的應(yīng)用也不是一時半會就會上手,很多情況下我們編寫的業(yè)務(wù)邏輯都沒用到設(shè)計模式或者本來就不需要特定的設(shè)計模式。
適配器模式
這個使我們常使用的設(shè)計模式,也算最簡單的設(shè)計模式之一,好處在于可以保持原有接口的數(shù)據(jù)結(jié)構(gòu)不變動。
適配器模式(Adapter Pattern)是作為兩個不兼容的接口之間的橋梁。
例子
適配器模式很好理解,假設(shè)我們和后端定義了一個接口數(shù)據(jù)結(jié)構(gòu)為(可以理解為舊接口):
- [
- {
- "label": "選擇一",
- "value": 0
- },
- {
- "label": "選擇二",
- "value": 1
- }
- ]
但是后端后面因?yàn)槠渌颍枰x返回的結(jié)構(gòu)為(可以理解為新接口):
- [
- {
- "label": "選擇一",
- "text": 0
- },
- {
- "label": "選擇二",
- "text": 1
- }
- ]
然后我們前端的使用到后端接口有好幾處,那么我可以把新的接口字段結(jié)構(gòu)適配為老接口的,就不需要各處去修改字段,只要把源頭的數(shù)據(jù)適配好就可以了。
當(dāng)然上面的是非常簡單的場景,也是經(jīng)常用到的場景。或許你會認(rèn)為后端處理不更好了,的確是這樣更好,但是這個不是我們討論的范圍。
單例模式
單例模式,從字面意思也很好理解,就是實(shí)例化多次都只會有一個實(shí)例。
有些場景實(shí)例化一次,可以達(dá)到緩存效果,可以減少內(nèi)存占用。還有些場景就是必須只能實(shí)例化一次,否則實(shí)例化多次會覆蓋之前的實(shí)例,導(dǎo)致出現(xiàn) bug(這種場景比較少見)。
例子
實(shí)現(xiàn)彈框的一種做法是先創(chuàng)建好彈框, 然后使之隱藏, 這樣子的話會浪費(fèi)部分不必要的 DOM 開銷, 我們可以在需要彈框的時候再進(jìn)行創(chuàng)建, 同時結(jié)合單例模式實(shí)現(xiàn)只有一個實(shí)例, 從而節(jié)省部分 DOM 開銷。下列為登入框部分代碼:
- const createLoginLayer = function() {
- const div = document.createElement('div')
- div.innerHTML = '登入浮框'
- div.style.display = 'none'
- document.body.appendChild(div)
- return div
- }
使單例模式和創(chuàng)建彈框代碼解耦
- const getSingle = function(fn) {
- const result
- return function() {
- return result || result = fn.apply(this, arguments)
- }
- }
- const createSingleLoginLayer = getSingle(createLoginLayer)
- document.getElementById('loginBtn').onclick = function() {
- createSingleLoginLayer()
- }
代理模式
代理模式的定義:為一個對象提供一個代用品或占位符,以便控制對它的訪問。
代理對象擁有本體對象的一切功能的同時,可以擁有而外的功能。而且代理對象和本體對象具有一致的接口,對使用者友好。
虛擬代理
下面這段代碼運(yùn)用代理模式來實(shí)現(xiàn)圖片預(yù)加載,可以看到通過代理模式巧妙地將創(chuàng)建圖片與預(yù)加載邏輯分離,,并且在未來如果不需要預(yù)加載,只要改成請求本體代替請求代理對象就行。
- const myImage = (function() {
- const imgNode = document.createElement('img')
- document.body.appendChild(imgNode)
- return {
- setSrc: function(src) {
- imgNode.src = src
- }
- }
- })()
- const proxyImage = (function() {
- const img = new Image()
- img.onload = function() { // http 圖片加載完畢后才會執(zhí)行
- myImage.setSrc(this.src)
- }
- return {
- setSrc: function(src) {
- myImage.setSrc('loading.jpg') // 本地 loading 圖片
- img.src = src
- }
- }
- })()
- proxyImage.setSrc('http://loaded.jpg')
緩存代理
在原有的功能上加上結(jié)果緩存功能,就屬于緩存代理。
原先有個功能是實(shí)現(xiàn)字符串反轉(zhuǎn)(reverseString),那么在不改變 reverseString 的現(xiàn)有邏輯,我們可以使用緩存代理模式實(shí)現(xiàn)性能的優(yōu)化,當(dāng)然也可以在值改變的時候去處理下其他邏輯,如 Vue computed 的用法。
- function reverseString(str) {
- return str
- .split('')
- .reverse()
- .join('')
- }
- const reverseStringProxy = (function() {
- const cached = {}
- return function(str) {
- if (cached[str]) {
- return cached[str]
- }
- cached[str] = reverseString(str)
- return cached[str]
- }
- })()
訂閱發(fā)布模式
訂閱發(fā)布使前端常用的數(shù)據(jù)通信方式、異步邏輯處理等等,如 React setState 和 Redux 就是訂閱發(fā)布模式的。
但是要合理的使用訂閱發(fā)布模式,否則會造成數(shù)據(jù)混亂,redux 的單向數(shù)據(jù)流思想可以避免數(shù)據(jù)流混亂的問題。
例子
- class Event {
- constructor() {
- // 所有 eventType 監(jiān)聽器回調(diào)函數(shù)(數(shù)組)
- this.listeners = {}
- }
- /**
- * 訂閱事件
- * @param {String} eventType 事件類型
- * @param {Function} listener 訂閱后發(fā)布動作觸發(fā)的回調(diào)函數(shù),參數(shù)為發(fā)布的數(shù)據(jù)
- */
- on(eventType, listener) {
- if (!this.listeners[eventType]) {
- this.listeners[eventType] = []
- }
- this.listeners[eventType].push(listener)
- }
- /**
- * 發(fā)布事件
- * @param {String} eventType 事件類型
- * @param {Any} data 發(fā)布的內(nèi)容
- */
- emit(eventType, data) {
- const callbacks = this.listeners[eventType]
- if (callbacks) {
- callbacks.forEach((c) => {
- c(data)
- })
- }
- }
- }
- const event = new Event()
- event.on('open', (data) => {
- console.log(data)
- })
- event.emit('open', { open: true })
觀察者模式
觀察者模式定義了一種一對多的依賴關(guān)系,讓多個觀察者對象同時監(jiān)聽某一個目標(biāo)對象,當(dāng)這個目標(biāo)對象的狀態(tài)發(fā)生變化時,會通知所有觀察者對象,使它們能夠自動更新。
Vue 的數(shù)據(jù)驅(qū)動就是使用觀察者模式,mbox 也是使用觀察者模式。
例子
模仿 Vue 數(shù)據(jù)驅(qū)動渲染模式(只是類似,簡單的模仿)。
首先使用 setter 和 getter 監(jiān)聽到數(shù)據(jù)的變化:
- const obj = {
- data: { description: '' },
- }
- Object.defineProperty(obj, 'description', {
- get() {
- return this.data.description
- },
- set(val) {
- this.data.description = val
- },
- })
然后加上目標(biāo)和觀察者
- class Subject {
- constructor() {
- this.observers = []
- }
- add(observer) {
- this.observers.push(observer)
- }
- notify(data) {
- this.observers.forEach((observer) => observer.update(data))
- }
- }
- class Observer {
- constructor(callback) {
- this.callback = callback
- }
- update(data) {
- this.callback && this.callback(data)
- }
- }
- // 創(chuàng)建觀察者ob1
- let ob1 = new Observer((text) => {
- document.querySelector('#dom-one').innerHTML(text)
- })
- // 創(chuàng)建觀察者ob2
- let ob2 = new Observer((text) => {
- document.querySelector('#dom-two').innerHTML(text)
- })
- // 創(chuàng)建目標(biāo)sub
- let sub = new Subject()
- // 目標(biāo)sub添加觀察者ob1 (目標(biāo)和觀察者建立了依賴關(guān)系)
- sub.add(ob1)
- // 目標(biāo)sub添加觀察者ob2
- sub.add(ob2)
- // 目標(biāo)sub觸發(fā)事件(目標(biāo)主動通知觀察者)
- sub.notify('這里改變了')
組合在一起是這樣的
- <!DOCTYPE html>
- <html>
- <head>
- <meta charset="utf-8" />
- <meta
- name="viewport"
- content="width=device-width,initial-scale=1,maximum-scale=1,viewport-fit=cover"
- />
- <title></title>
- </head>
- <body>
- <div id="app">
- <div id="dom-one">
- 原來的值
- </div>
- <br />
- <div id="dom-two">
- 原來的值
- </div>
- <br />
- <button id="btn">改變</button>
- </div>
- <script>
- class Subject {
- constructor() {
- this.observers = []
- }
- add(observer) {
- this.observers.push(observer)
- }
- notify() {
- this.observers.forEach((observer) => observer.update())
- }
- }
- class Observer {
- constructor(callback) {
- this.callback = callback
- }
- update() {
- this.callback && this.callback()
- }
- }
- const obj = {
- data: { description: '' },
- }
- // 創(chuàng)建觀察者ob1
- const ob1 = new Observer(() => {
- console.log(document.querySelector('#dom-one'))
- document.querySelector('#dom-one').innerHTML = obj.description
- })
- // 創(chuàng)建觀察者ob2
- const ob2 = new Observer(() => {
- document.querySelector('#dom-two').innerHTML = obj.description
- })
- // 創(chuàng)建目標(biāo)sub
- const sub = new Subject()
- // 目標(biāo)sub添加觀察者ob1 (目標(biāo)和觀察者建立了依賴關(guān)系)
- sub.add(ob1)
- // 目標(biāo)sub添加觀察者ob2
- sub.add(ob2)
- Object.defineProperty(obj, 'description', {
- get() {
- return this.data.description
- },
- set(val) {
- this.data.description = val
- // 目標(biāo)sub觸發(fā)事件(目標(biāo)主動通知觀察者)
- sub.notify()
- },
- })
- btn.onclick = () => {
- obj.description = '改變了'
- }
- </script>
- </body>
- </html>
裝飾者模式
裝飾器模式(Decorator Pattern)允許向一個現(xiàn)有的對象添加新的功能,同時又不改變其結(jié)構(gòu)。
ES6/7 的decorator 語法提案,就是裝飾者模式。
例子
- class A {
- getContent() {
- return '第一行內(nèi)容'
- }
- render() {
- document.body.innerHTML = this.getContent()
- }
- }
- function decoratorOne(cla) {
- const prevGetContent = cla.prototype.getContent
- cla.prototype.getContent = function() {
- return `
- 第一行之前的內(nèi)容
- <br/>
- ${prevGetContent()}
- `
- }
- return cla
- }
- function decoratorTwo(cla) {
- const prevGetContent = cla.prototype.getContent
- cla.prototype.getContent = function() {
- return `
- ${prevGetContent()}
- <br/>
- 第二行內(nèi)容
- `
- }
- return cla
- }
- const B = decoratorOne(A)
- const C = decoratorTwo(B)
- new C().render()
策略模式
在策略模式(Strategy Pattern)中,一個行為或其算法可以在運(yùn)行時更改。
假設(shè)我們的績效分為 A、B、C、D 這四個等級,四個等級的獎勵是不一樣的,一般我們的代碼是這樣實(shí)現(xiàn):
- /**
- * 獲取年終獎
- * @param {String} performanceType 績效類型,
- * @return {Object} 年終獎,包括獎金和獎品
- */
- function getYearEndBonus(performanceType) {
- const yearEndBonus = {
- // 獎金
- bonus: '',
- // 獎品
- prize: '',
- }
- switch (performanceType) {
- case 'A': {
- yearEndBonus = {
- bonus: 50000,
- prize: 'mac pro',
- }
- break
- }
- case 'B': {
- yearEndBonus = {
- bonus: 40000,
- prize: 'mac air',
- }
- break
- }
- case 'C': {
- yearEndBonus = {
- bonus: 20000,
- prize: 'iphone xr',
- }
- break
- }
- case 'D': {
- yearEndBonus = {
- bonus: 5000,
- prize: 'ipad mini',
- }
- break
- }
- }
- return yearEndBonus
- }
使用策略模式可以這樣:
- /**
- * 獲取年終獎
- * @param {String} strategyFn 績效策略函數(shù)
- * @return {Object} 年終獎,包括獎金和獎品
- */
- function getYearEndBonus(strategyFn) {
- if (!strategyFn) {
- return {}
- }
- return strategyFn()
- }
- const bonusStrategy = {
- A() {
- return {
- bonus: 50000,
- prize: 'mac pro',
- }
- },
- B() {
- return {
- bonus: 40000,
- prize: 'mac air',
- }
- },
- C() {
- return {
- bonus: 20000,
- prize: 'iphone xr',
- }
- },
- D() {
- return {
- bonus: 10000,
- prize: 'ipad mini',
- }
- },
- }
- const performanceLevel = 'A'
- getYearEndBonus(bonusStrategy[performanceLevel])
這里每個函數(shù)就是一個策略,修改一個其中一個策略,并不會影響其他的策略,都可以單獨(dú)使用。當(dāng)然這只是個簡單的范例,只為了說明。
策略模式比較明顯的特性就是可以減少 if 語句或者 switch 語句。
職責(zé)鏈模式
顧名思義,責(zé)任鏈模式(Chain of Responsibility Pattern)為請求創(chuàng)建了一個接收者對象的鏈。這種模式給予請求的類型,對請求的發(fā)送者和接收者進(jìn)行解耦。這種類型的設(shè)計模式屬于行為型模式。
在這種模式中,通常每個接收者都包含對另一個接收者的引用。如果一個對象不能處理該請求,那么它會把相同的請求傳給下一個接收者,依此類推。
例子
- function order(options) {
- return {
- next: (callback) => callback(options),
- }
- }
- function order500(options) {
- const { orderType, pay } = options
- if (orderType === 1 && pay === true) {
- console.log('500 元定金預(yù)購, 得到 100 元優(yōu)惠券')
- return {
- next: () => {},
- }
- } else {
- return {
- next: (callback) => callback(options),
- }
- }
- }
- function order200(options) {
- const { orderType, pay } = options
- if (orderType === 2 && pay === true) {
- console.log('200 元定金預(yù)購, 得到 50 元優(yōu)惠券')
- return {
- next: () => {},
- }
- } else {
- return {
- next: (callback) => callback(options),
- }
- }
- }
- function orderCommon(options) {
- const { orderType, stock } = options
- if (orderType === 3 && stock > 0) {
- console.log('普通購買, 無優(yōu)惠券')
- return {}
- } else {
- console.log('庫存不夠, 無法購買')
- }
- }
- order({
- orderType: 3,
- pay: true,
- stock: 500,
- })
- .next(order500)
- .next(order200)
- .next(orderCommon)
- // 打印出 “普通購買, 無優(yōu)惠券”
上面的代碼,對 order 相關(guān)的進(jìn)行了解耦,order500,order200、orderCommon 等都是可以單獨(dú)調(diào)用的。