前言
記得剛畢業的時候參加了一次校招面試,之前表現的很好,最後時面試官問我懂不懂設計模式,我說不懂,然後就進去了;後面又參加了某大公司的校招,開始表現還行,後面面試官問我懂不懂設計模式,我說懂(上次後補習了下),最後把工廠模式的程式碼背寫到了紙上,然後就沒有然後了......
現在回想起來當時有點傻有點天真,沒有幾十萬的程式碼量,沒有一定的經驗總結,居然敢說懂設計模式,這不是找抽麼?
現在經過幾年工作學習,倒是是時候系統的回憶下平時工作內容,看用到了什麼設計模式,權當總結。
小釵對設計模式的理解程度有限,文中不足之處請您拍磚。
物件導向的實現
設計模式便是物件導向的深入,物件導向的應用,所以類的實現是第一步:
PS:這裡依賴了underscore,各位自己加上吧。
1 //window._ = _ || {}; 2 // 全域性可能用到的變數 3 var arr = []; 4 var slice = arr.slice; 5 /** 6 * inherit方法,js的繼承,預設為兩個引數 7 * 8 * @param {function} origin 可選,要繼承的類 9 * @param {object} methods 被建立類的成員,擴充套件的方法和屬性 10 * @return {function} 繼承之後的子類 11 */ 12 _.inherit = function (origin, methods) { 13 14 // 引數檢測,該繼承方法,只支援一個引數建立類,或者兩個引數繼承類 15 if (arguments.length === 0 || arguments.length > 2) throw '引數錯誤'; 16 17 var parent = null; 18 19 // 將引數轉換為陣列 20 var properties = slice.call(arguments); 21 22 // 如果第一個引數為類(function),那麼就將之取出 23 if (typeof properties[0] === 'function') 24 parent = properties.shift(); 25 properties = properties[0]; 26 27 // 建立新類用於返回 28 function klass() { 29 if (_.isFunction(this.initialize)) 30 this.initialize.apply(this, arguments); 31 } 32 33 klass.superclass = parent; 34 35 // 父類的方法不做保留,直接賦給子類 36 // parent.subclasses = []; 37 38 if (parent) { 39 // 中間過渡類,防止parent的建構函式被執行 40 var subclass = function () { }; 41 subclass.prototype = parent.prototype; 42 klass.prototype = new subclass(); 43 44 // 父類的方法不做保留,直接賦給子類 45 // parent.subclasses.push(klass); 46 } 47 48 var ancestor = klass.superclass && klass.superclass.prototype; 49 for (var k in properties) { 50 var value = properties[k]; 51 52 //滿足條件就重寫 53 if (ancestor && typeof value == 'function') { 54 var argslist = /^\s*function\s*\(([^\(\)]*?)\)\s*?\{/i.exec(value.toString())[1].replace(/\s/i, '').split(','); 55 //只有在第一個引數為$super情況下才需要處理(是否具有重複方法需要使用者自己決定) 56 if (argslist[0] === '$super' && ancestor[k]) { 57 value = (function (methodName, fn) { 58 return function () { 59 var scope = this; 60 var args = [ 61 function () { 62 return ancestor[methodName].apply(scope, arguments); 63 } 64 ]; 65 return fn.apply(this, args.concat(slice.call(arguments))); 66 }; 67 })(k, value); 68 } 69 } 70 71 //此處對物件進行擴充套件,當前原型鏈已經存在該物件,便進行擴充套件 72 if (_.isObject(klass.prototype[k]) && _.isObject(value) && (typeof klass.prototype[k] != 'function' && typeof value != 'fuction')) { 73 //原型鏈是共享的,這裡處理邏輯要改 74 var temp = {}; 75 _.extend(temp, klass.prototype[k]); 76 _.extend(temp, value); 77 klass.prototype[k] = temp; 78 } else { 79 klass.prototype[k] = value; 80 } 81 82 } 83 84 if (!klass.prototype.initialize) 85 klass.prototype.initialize = function () { }; 86 87 klass.prototype.constructor = klass; 88 89 return klass; 90 };
使用測試:
1 var Person = _.inherit({ 2 initialize: function(opts) { 3 this.setOpts(opts); 4 }, 5 6 setOpts: function (opts) { 7 for(var k in opts) { 8 this[k] = opts[k]; 9 } 10 }, 11 12 getName: function() { 13 return this.name; 14 }, 15 16 setName: function (name) { 17 this.name = name 18 } 19 }); 20 21 var Man = _.inherit(Person, { 22 initialize: function($super, opts) { 23 $super(opts); 24 this.sex = 'man'; 25 }, 26 27 getSex: function () { 28 return this.sex; 29 } 30 }); 31 32 var Woman = _.inherit(Person, { 33 initialize: function($super, opts) { 34 $super(opts); 35 this.sex = 'women'; 36 }, 37 38 getSex: function () { 39 return this.sex; 40 } 41 }); 42 43 var xiaoming = new Man({ 44 name: '小明' 45 }); 46 47 var xiaohong = new Woman({ 48 name: '小紅' 49 });
xiaoming.getName() "小明" xiaohong.getName() "小紅" xiaoming.getSex() "man" xiaohong.getSex() "women"
單例模式(Singleton)
單列為了保證一個類只有一個例項,如果不存在便直接返回,如果存在便返回上一次的例項,其目的一般是為了資源優化。
javascript中實現單例的方式比較多,比較實用的是直接使用物件字面量:
1 var singleton = { 2 property1: "property1", 3 property2: "property2", 4 method1: function () {} 5 };
類實現是正統的實現,一般是放到類上,做靜態方法:
在實際專案中,一般這個應用會在一些通用UI上,比如mask,alert,toast,loading這類元件,還有可能是一些請求資料的model,簡單程式碼如下:
1 //唯一標識,一般在amd模組中 2 var instance = null; 3 4 //js不存在多執行緒,這裡是安全的 5 var UIAlert = _.inherit({ 6 initialize: function(msg) { 7 this.msg = msg; 8 }, 9 setMsg: function (msg) { 10 this.msg = msg; 11 }, 12 showMessage: function() { 13 console.log(this.msg); 14 } 15 }); 16 17 var m1 = new UIAlert('1'); 18 m1.showMessage();//1 19 var m2 = new UIAlert('2'); 20 m2.showMessage();//2 21 m1.showMessage();//1
如所示,這個是一個簡單的應用,如果稍作更改的話:
1 //唯一標識,一般在amd模組中 2 var instance = null; 3 4 //js不存在多執行緒,這裡是安全的 5 var UIAlert = _.inherit({ 6 initialize: function(msg) { 7 this.msg = msg; 8 }, 9 setMsg: function (msg) { 10 this.msg = msg; 11 }, 12 showMessage: function() { 13 console.log(this.msg); 14 } 15 }); 16 UIAlert.getInstance = function () { 17 if (instance instanceof this) { 18 return instance; 19 } else { 20 return instance = new UIAlert(); //new this 21 } 22 } 23 24 var m1 = UIAlert.getInstance(); 25 m1.setMsg(1); 26 m1.showMessage();//1 27 var m2 = UIAlert.getInstance(); 28 m2.setMsg(2); 29 m2.showMessage();//2 30 m1.showMessage();//2
如所示,第二次的改變影響了m1的值,因為他們的例項msg是共享的,這個便是一次單列的使用,而實際場景複雜得多。
以alert元件為例,他還會存在按鈕,一個、兩個或者三個,每個按鈕事件回撥不一樣,一次設定後,第二次使用時各個事件也需要被重置,比如事件裝在一個陣列eventArr = []中,每次這個陣列需要被清空重置,整個元件的dom結構也會重置,好像這個單例的意義也減小了,真實情況是這樣的意義在於全站,特別是對於webapp的網站,只有一個UI dom的根節點,這個才是該場景的意義所在。
而對mask而言便不太適合全部做單例,以彈出層UI來說,一般都會帶有一個mask元件,如果一個元件彈出後馬上再彈出一個,第二個mask如果與第一個共享的話便不合適了,因為這個mask應該是各元件獨享的。
單例在javascript中的應用更多的還是來劃分名稱空間,比如underscore庫,比如以下場景:
① Hybrid橋接的程式碼
window.Hybrid = {};//存放所有Hybrid的引數
② 日期函式
window.DateUtil = {};//存放一些日期操作方法,比如將“2015年2月14日”這類字串轉換為日期物件,或者逆向轉換
......
工廠模式(Factory)
工廠模式是一個比較常用的模式,介於javascript物件的不定性,其在前端的應用門檻更低。
工廠模式出現之初意在解決物件耦合問題,通過工廠方法,而不是new關鍵字例項化具體類,將所有可能的類的例項化集中在一起。
一個最常用的例子便是我們的Ajax模組:
1 var XMLHttpFactory = {}; 2 var XMLHttpFactory.createXMLHttp = function() { 3 var XMLHttp = null; 4 if (window.XMLHttpRequest){ 5 XMLHttp = new XMLHttpRequest() 6 }else if (window.ActiveXObject){ 7 XMLHttp = new ActiveXObject("Microsoft.XMLHTTP") 8 } 9 return XMLHttp; 10 }
使用工廠方法的前提是,產品類的介面需要一致,至少公用介面是一致的,比如我們這裡有一個需求是這樣的:
可以看到各個模組都是不一樣的:
① 資料請求
② dom渲染,樣式也有所不同
③ 事件互動
但是他們有一樣是相同的:會有一個共同的事件點:
① create
② show
③ hide
所以我們的程式碼可以是這樣的:
1 var AbstractView = _.inherit({ 2 initialize: function() { 3 this.wrapper = $('body'); 4 //事件管道,例項化時觸發onCreate,show時候觸發onShow...... 5 this.eventsArr = []; 6 }, 7 show: function(){}, 8 hide: function (){} 9 }); 10 var SinaView = _.inherit(AbstractView, { 11 }); 12 var BaiduView = _.inherit(AbstractView, { 13 });
每一個元件例項化只需要執行例項化操作與show操作即可,各個view的顯示邏輯在自己的事件管道實現,真實的邏輯可能是這樣的
1 var ViewContainer = { 2 SinaView: SinaView, 3 BaiduView: BaiduView 4 }; 5 var createView = function (view, wrapper) { 6 //這裡會有一些監測工作,事實上所有的view類應該放到一個單列ViewContainer中 7 var ins = new ViewContainer[view + 'View']; 8 ins.wrapper = wrapper; 9 ins.show(); 10 } 11 //資料庫讀出資料 12 var moduleInfo = ['Baidu', 'Sina', '...']; 13 14 for(var i = 0, len = moduleInfo.length; i < len; i++){ 15 createView(moduleInfo[i]); 16 }
如之前寫的坦克大戰,建立各自坦克工廠模式也是絕佳的選擇,工廠模式暫時到此。
橋接模式(bridge)
橋接模式一個非常典型的使用便是在Hybrid場景中,native同事會給出一個用於橋接native與H5的模組,一般為bridge.js。
native與H5本來就是互相獨立又互相變化的,如何在多個維度的變化中又不引入額外複雜度,這個時候bridge模式便派上了用場,使抽象部分與實現部分分離,各自便能獨立變化。
這裡另舉一個應用場景,便是UI與其動畫類,UI一般會有show的動作,通常便直接顯示了出來,但是我們實際工作中需要的UI顯示是:由下向上動畫顯示,由上向下動畫顯示等效果。
這個時候我們應該怎麼處理呢,簡單設計一下:
1 var AbstractView = _.inherit({ 2 initialize: function () { 3 //這裡的dom其實應該由template於data組成,這裡簡化 4 this.$el = $('<div style="display: none; position: absolute; left: 100px; top: 100px; border: 1px solid #000000;">元件</div>'); 5 this.$wrapper = $('body'); 6 this.animatIns = null; 7 }, 8 show: function () { 9 this.$wrapper.append(this.$el); 10 if(!this.animatIns) { 11 this.$el.show(); 12 } else { 13 this.animatIns.animate(this.$el, function(){}); 14 } 15 //this.bindEvents(); 16 } 17 }); 18 19 var AbstractAnimate = _.inherit({ 20 initialize: function () { 21 }, 22 //override 23 animate: function (el, callback) { 24 el.show(); 25 callback(); 26 } 27 }); 28 29 30 var UPToDwonAnimate = _.inherit(AbstractAnimate, { 31 animate: function (el, callback) { 32 //動畫具體實現不予關注,這裡使用zepto實現 33 el.animate({ 34 'transform': 'translate(0, -250%)' 35 }).show().animate({ 36 'transform': 'translate(0, 0)' 37 }, 200, 'ease-in-out', callback); 38 } 39 }); 40 41 42 var UIAlert = _.inherit(AbstractView, { 43 initialize: function ($super, animateIns) { 44 $super(); 45 this.$el = $('<div style="display: none; position: absolute; left: 100px; top: 200px; border: 1px solid #000000;">alert元件</div>'); 46 this.animatIns = animateIns; 47 } 48 }); 49 50 var UIToast = _.inherit(AbstractView, { 51 initialize: function ($super, animateIns) { 52 $super(); 53 this.animatIns = animateIns; 54 } 55 }); 56 57 var t = new UIToast(new UPToDwonAnimate); 58 t.show(); 59 60 var a = new UIAlert(); 61 a.show();
這裡元件對動畫類庫有依賴,但是各自又不互相影響(事實上還是有一定影響的,比如其中一些事件便需要動畫引數觸發),這個便是一個典型的橋接模式。
再換個方向理解,UI的css樣式事實上也可以做到兩套系統,一套dom結構一套皮膚庫,但是這個實現上有點複雜,因為html不可分割,而動畫功能這樣處理卻比較合適。
裝飾者模式(decorator)
裝飾者模式的意圖是為一個物件動態的增加一些額外職責;是類繼承的另外一種選擇,一個是編譯時候增加行為,一個是執行時候。
裝飾者要求其實現與包裝的物件統一,並做到過程透明,意味著可以用他來包裝其他物件,而使用方法與原來一致。
一次邏輯的執行可以包含多個裝飾物件,這裡舉個例子來說,在webapp中每個頁面的view往往會包含一個show方法,而在我們的頁面中我們可能會根據localsorage或者ua判斷要不要顯示下面廣告條,效果如下:
那麼這個邏輯應該如何實現呢?
1 var View = _.inherit({ 2 initialize: function () {}, 3 show: function () { 4 console.log('渲染基本頁面'); 5 } 6 }); 7 8 //廣告裝飾者 9 var AdDecorator = _.inherit({ 10 initialize: function (view) { 11 this.view = view; 12 }, 13 show: function () { 14 this.view.show(); 15 console.log('渲染廣告區域'); 16 } 17 }); 18 19 //基本使用 20 var v = new View(); 21 v.show(); 22 23 //........ .滿足一定條件........... 24 var d = new AdDecorator(v); 25 d.show();
說實話,就站在前端的角度,以及我的視野來說,這個裝飾者其實不太實用,換個說法,這個裝飾者模式非常類似面向切口程式設計,就是在某一個點前做點事情,後做點事情,這個時候事件管道似乎更加合適。
結語
今天回顧了單例、工廠、橋接、裝飾者模式,我們後面再繼續,文中有何不足請您指教。