相信大家在平時的開發中或多或少聽說的聽說過各種設計模式,有非常常見的,比如:觀察者模式,工廠模式,裝飾器模式等等,熟練掌握設計模式不僅僅對我們技術提升有巨大的幫助,而且更有利于編程思維的提升,而且如果我們能夠熟練的將各種設計模式應用到實際的開發中,對代碼的易讀和維護都能夠起到積極的作用。
設計原則
Tips:在學習設計模式的時候,盡量分開去學,即先學習設計,然后再去學模式,這樣的話,對于之后的理解更加容易一些,什么設計呢,其實就是指設計原則,而模式這里指的就是就是我們要講的設計模式。現在共有5大設計原則,不管是哪種設計模式,都是遵循設計原則的。下面來分別介紹這5大設計原則。
1.單一職責原則
單一職責原則原則就是每個程序只負責做好一件事情,如果功能過于復雜就拆分開,每個部分保持獨立。這個其實也符合我們當下流行框架Vue和React的組件化開發,把一個復雜的頁面拆分成一些零散的組件,并且每個組件保持獨立,同時也可在不同的頁面當中實現復用。
2.開放封閉原則
開發封閉原則大白話的意思就是對擴展開放,對修改封閉。放到實際開發中如何去理解呢,我們日常的網站和App開發每周都有發不同的版本來增加需求,那么增加需求的時候,盡量要做到擴展新代碼,而非修改已有代碼,如果我們修改已有代碼無疑增加了風險,因為本來原來的代碼是沒有問題的,加了新的代碼之后必然會增加不可預知的風險,當然有的個別需求必須修改已有代碼,這個另說。同時這個原則也是我們軟件設計的終極目標。
3.李氏置換原則
子類能夠覆蓋父類,父類能出現的地方,子類就可以出現,這個原則其實在JAVA等語言當中是較為常見的,多用于繼承,而JavaScript作為弱類型語言,繼承使用其實是很少的,這里簡單提一下。
4.接口獨立原則
接口獨立原則的含義是保持接口的單一獨立,避免出現胖接口,JavaScript中是沒有接口(typescript例外),使用較少, 它是有點類似于單一職責原則,這里更關注接口。
5.依賴倒置原則
依賴倒置原則的含義是面向接口編程,依賴于抽象而不依賴于具體,使用方只關注接口而不關注具體類的實現,同樣這里也是JavaScript中使用較少(沒有接口&弱類型)
設計模式
設計模式分類
- 創建型
工廠模式(工廠方法模式、抽象工廠模式、建造者模式) 單例模式
- 組合型
- 適配器模式 裝飾器模式 代理模式 外觀模式
- 行為型
- 觀察者模式 狀態模式
下面我們就來一一結合開發當中的實例,來分析各個設計模式
工廠模式
概念
工廠模式是由一個方法來確定是要創建哪個類的實例,在前端當中最為常見的工廠模式就是new操作的單獨封裝,當遇到new操作的時候,就要考慮是否該使用工廠模式。這里也可以結合生活中的例子去思考。當你去購買漢堡,直接點餐取餐,不會自己親手做,商店要“封裝”做漢堡的工作,做好直接給買者。也就是說通過提供原材料,最終得到是漢堡還是炸雞,是由你自己決定的。
前端中實例
1. jQuery當中的$('')
jQuery當中的$('div'),這里的$選擇器就是已經封裝好的API,這里我們直接使用即可。下面簡單實現一個JQuery的$操作符,幫助大家加深理解。
class jQuery { constructor(selector) { let slice = Array.prototype.slice let dom = slice.call(document.querySelectorAll(selector)) let len = dom ? dom.length : 0 for(let i = 0;i < len; i++) { this[i] = dom[i] } this.length = len this.selector = selector || '' } append(node) { } html(data) { } //等等API } window.$ = function (selector) { return new jQuery(selector) }
2. Vue異步組件
這個大家應該比較熟悉,而且官方文檔講的也非常詳細,這里直接飲用官方文檔的案例,在大型應用中,我們可能需要將應用分割成小一些的代碼塊,并且只在需要的時候才從服務器加載一個模塊。為了簡化,Vue 允許你以一個工廠函數的方式定義你的組件,這個工廠函數會異步解析你的組件定義。Vue 只有在這個組件需要被渲染的時候才會觸發該工廠函數,且會把結果緩存起來供未來重渲染。例如:
Vue.component('async-example', function (resolve, reject) { setTimeout(function () { // 向 `resolve` 回調傳遞組件定義 resolve({ template: '<div>I am async!</div>' }) }, 1000) })
如你所見,這個工廠函數會收到一個 resolve 回調,這個回調函數會在你從服務器得到組件定義的時候被調用。你也可以調用 reject(reason) 來表示加載失敗。這里的 setTimeout 是為了演示用的,如何獲取組件取決于你自己。一個推薦的做法是將異步組件和 webpack 的 code-splitting 功能一起配合使用:
Vue.component('async-webpack-example', function (resolve) { // 這個特殊的 `require` 語法將會告訴 webpack // 自動將你的構建代碼切割成多個包,這些包 // 會通過 Ajax 請求加載 require(['./my-async-component'], resolve) })
你也可以在工廠函數中返回一個 Promise,所以把 webpack 2 和 ES2015 語法加在一起,我們可以寫成這樣:
Vue.component( 'async-webpack-example', // 這個 `import` 函數會返回一個 `Promise` 對象。 () => import('./my-async-component') )
當使用局部注冊的時候,你也可以直接提供一個返回 Promise 的函數:
new Vue({ // ... components: { 'my-component': () => import('./my-async-component') } })
單例模式
概念
單例模式符合單一職責原則,用大白話描述單例模式就是系統中被唯一使用,例如:電子商務網站常見的購物車和登錄都是單例模式的運用。
前端中的實例
1.jQuery中的$('')
仍舊是jQuery當中的$(' ')選擇器,整個jQuery框架當中有一個這樣的選擇器。
2.Redux和Vuex
不管是Redux還是Vuex,里面的狀態store都是唯一的,Redux中的store只能通過Reducer去修改,而Vuex中的store只能通過Mutation修改,其余修改方式都是錯誤的。
適配器模式
概念
適配器模式的含義是舊接口格式和使用者不兼容,中間加一個適配器接口。生活當中隨處可見符合適配器模式的例子,如:插頭轉換器,電腦接口轉換器。
前端中的實例
封裝舊的接口
這里我來列舉一個例子,這里那我們常用的發起ajax請求為例,你自己封裝的ajax,使用方式如下:
ajax({ url:'/getList', type:'Post', dataType:'json', data:{ id:"123" } }) .done(function(){})
但是這個時候你接到的項目當中都是:$.ajax({...}),這個時候我們只需要加一層適配器即可,代碼如下:
let $ = {
ajax:function (options) {
return ajax(options);
}
}
Vue中的computed
Vue的計算屬性相信大家在項目的開發當中都是經常會用到的一個特性,比如一個字符串我們想要他的翻轉后的結果,那么這里就可以使用計算屬性,計算屬性這個特性本身用的設計模式就是適配器模式,代碼如下:
<div id="example"> <p>Original message: "{{ message }}"</p> <p>Computed reversed message: "{{ reversedMessage }}"</p> </div> var vm = new Vue({ el: '#example', data: { message: 'Hello' }, computed: { // 計算屬性的 getter reversedMessage: function () { // `this` 指向 vm 實例 return this.message.split('').reverse().join('') } } })
結果:
Original message: "Hello"
Computed reversed message: "olleH"
裝飾器模式
概念
裝飾器模式,裝飾我們可以理解為就是給一個東西裝飾另外一些東西使其更好看,對應到前端中就是為對象添加新功能,并且不改變其原有的結構和功能,這種模式創建了一個裝飾類,用來包裝原有的類,并在保持類方法簽名完整性的前提下,提供了額外的功能。在我們日常生活中用到的手機殼就是經典的例子
前端中的實例
1.ES7中的裝飾器
ES7裝飾器的具體用法可以去找阮一峰老師的相關文章,其實參照其字面意思,就是對類、方法、屬性進行修飾,從而進行一些相關功能的定制。那么JavaScript的Decorator在原理和功能上簡單明了,簡單來說就是對對象進行包裝,返回一個新的對象描述,這里可以類比高階組件。這里我們列舉一個簡單的例子,想必大家都玩過王者榮耀,英雄的戰斗力都是隨著裝備和等級的增加越來越厲害,那么這里我們就以王者榮耀當中的英雄為例,示例場景是這樣的:
- 首先創建一個普通的英雄類,他的防御值為10,攻擊力為20,血量為20;
- 然后我們給它出布甲裝備,這樣它的抵御力增加100,變為110;
然后按照這個場景來寫具體的代碼:
創建Hero類
class Hero { constructor(define = 10,attack = 20, blood = 20) { this.init(define,attack,blood) } init(define,attack,blood) { this.define = define; this.attack = attack; this.blood = blood; } toString() { return `防御力: ${this.define},攻擊力:${this.attack},血量:${this.blood}` } } let houyi = new Hero(); console.log(`當前狀態 ===> ${houyi}`) //輸出:當前狀態 ===> 防御力:10,攻擊力20,血量20
創建decorateCloth方法,為英雄增加布甲裝備。
function decorateCloth(target,key,descriptor) { const method= descriptor.value; let moreDef = 100; let ret; descriptor.value = (...args) => { args[0] += moreDef; ret = method.apply(target,args); return ret; } return descriptor; } class Hero { constructor(define = 10,attack = 20, blood = 20) { this.init(define,attack,blood) } @decorateCloth init(define,attack,blood) { this.define = define; this.attack = attack; this.blood = blood; } toString() { return `防御力: ${this.define},攻擊力:${this.attack},血量:${this.blood}` } } let houyi = new Hero(); console.log(`當前狀態 ===> ${houyi}`) //輸出:當前狀態 ===> 防御力:110,攻擊力20,血量20
可以看到輸出結果防御力確實增加了,布甲確實起到了作用。
從上面的代碼可以看出,如果有的時候我們并不需要關心函數的內部實現,僅僅是想調用它的話,裝飾器能夠帶來比較好的可讀性,使用起來也是非常的方便。
代理模式
概念
代理模式是使用者無權訪問目標對象,中間加代理,通過代理做授權和控制,我們經常會用到這個模式,不管實際的開發也好,還是網絡部署當中,都能夠看到它的身影。如:科學上網 谷歌搜索
前端中的實例
1.網頁中的事件代理
其實網頁的事件代理也是非常常考的面試題之一,其實就是把元素綁定到父元素上面,而不是對其下面的每一個子元素都進行相應的綁定,下面舉一個具體的實例:
<div id="item"> <a href="#">a1</a> <a href="#">a2</a> <a href="#">a3</a> <a href="#">a4</a> <div> <button>點擊增加一個a標簽</button> <script> let div1 = document.getElementById('div1') div1.addEventListener('click',function(e) { let target = e.target if(e.nodeName === 'A') { alert(target.innerHTML) } }) </script>
2.jQuery中的$proxy
比如我們經常會遇到這樣一種情況,如下代碼所示
$('#div1').click(function() { $(this).addClass('red') }) $('#div1').click(function() { setTimeout(function () { // this 不符合期望 $(this).addClass('red'); },1000); })
解決的方法可能有的同學已經想到是先將this賦值給一個變量。
$('#div1').click(function() { setTimeout(function () { let _this = this; // this 不符合期望 $(_this).addClass('red'); },1000); })
是的這種方法是對的,但是這樣就會增加一個變量,所以這里用$proxy解決更好,代碼如下:
$('#div1').click(function() { setTimeout($proxy(function () { $(this).addClass('red'); }),1000); })
觀察者模式
概念
觀察者模式就是只要你作為訂閱者訂閱了某個事件,當事件觸發的時候,發布者就會通知你。這里可以類比我們去咖啡廳點咖啡,當我們點了咖啡之后,就可以去做別的事情,當咖啡完成時,服務員就會叫你來取,你到時候取走咖啡即可。
前端中的實例
1.網頁中的事件綁定
<button id="btn1">btn</button> <scirpt> $('#btn1').click(function() { console.log(1) }) </script>
這里我們訂閱了btn1的click事件,當幾點btn1所在的元素時,click function就會觸發。
2.Node.js的自定義事件EventEmitter
const EventEmitter = require('events').EventEmitter const emitter1 = new EventEmitter(); emitter1.on('some',()=> { //監聽some事件 console.log('some event is occured 1') }) emitter1.on('some',()=> { //監聽some事件 console.log('some event is occured 2') }) emitter.emit('some')
狀態模式
概念
提到狀態模式就不得不提到有限狀態機,阮一峰總結過有限狀態機的三個特征:
- 狀態總數(state)是有限的。
- 任一時刻,只處在一種狀態之中。
- 某種條件下,會從一種狀態轉變(transition)到另一種狀態。
這里充分說明了狀態模式的重要性。一個對象有狀態變化,每次狀態變化都會觸發一個邏輯,不能總是用if else來控制,更要學會用狀態來進行控制。
前端中的實例
1.ES6中的promise
promise是ES6新增的一個特性,它是異步編程的一種解決方案,比傳統的解決方案更加方便,可以使用.then()操作避免了回調嵌套帶來的回調地獄式的寫法,那么下面就來簡單實現一個promise。
let fsm = new statemachine({ init:'pending', transitions:[ { name:'resolve', from:'pending', to:'fullfilled' }, { name:'reject', from:'pending', to:'rejected' } ], methods:{ onResolve:function(state,data) { data.successList.forEach(fn => fn()) } onReject:function(state,data) { data.failList.forEach(fn => fn()) } } })
這就是一個簡單的promise。一個promise只有三種狀態,所以promise可以說是狀態模式的經典案例。
總結
這篇文章我們主要介紹了5大設計原則和7個設計模式,這7個設計模式是前端使用頻率較高的設計模式,當然還有很多其他的設計模式,由于篇幅原因這里無法全部介紹,