接觸前端兩三個月的時候,那時候只是據說設計模式很重要,而後我就去讀了一本設計模式的書,讀了一部分,也不知道這些設計模式到底設計出來幹嗎的,而後就沒再看了。後來就本身作一些小項目也覺着好像不須要用到設計模式這個東西呀。如今,接觸前端有半年了,決定再從新看看設計模式,說不定會有一些啓發。因而發現了一本好書——《JavaScript設計模式》,寫的通俗易懂,用一個個故事串起了一整本書,看了一部分發現原來我平時寫代碼的時候無心之中就用到了一些設計模式,而後就忍不住都看完了。看完整本書,讓我徹底改變了之前對設計模式的見解,也學到了不少在實際項目開發中的經驗。這裏就簡單總結下這本書,也算是作個筆記,供本身之後參考。(定義通常都比較晦澀難懂,能夠先看看使用場景再回來理解相關定義)
先給個書的連接: JavaScript設計模式-張容銘javascript
設計模式是代碼設計經驗的總結,爲了可重用代碼,保證代碼的可靠性等。設計模式主要分爲三大類型,建立型模式,結構型模式和行爲型模式,本書還額外寫了另兩類設計模式,技巧型模式和架構型模式。JavaScript設計模式是以面向對象編程爲基礎的,JavaScript的面向對象編程和傳統的C++、Java的面向對象編程有些差異,這讓我一開始接觸JavaScript的時候感到十分痛苦,可是這隻能靠本身慢慢積累慢慢思考。想繼續瞭解JavaScript設計模式必需要先搞懂JavaScript面向對象編程,不然只會讓你本身更痛苦。php
建立型設計模式是一類處理對象建立的設計模式,經過某種方式控制對象的建立來避免基本對象建立時可能致使設計上的問題或增長設計上的複雜度。建立型設計模式主要有簡單工廠模式,工廠方法模式,抽象工廠模式,建造者模式,原型模式和單例模式,下面一一道來。css
做者把簡單工廠模式比喻成一個神奇的魔術師。html
又叫靜態工廠方法,由一個工廠對象決定建立某一種產品對象類的實例,主要用來建立同一類對象。前端
看完上面的定義必定很不解,說的究竟是啥,如今就舉個例子來解釋一下。好比體育商品店賣體育器材,裏面有不少體育用品及其相關介紹。當你來到體育用品店買一個籃球,只需問售貨員,他就會幫你找到你所要的東西。用程序實現以下:vue
// 籃球基類 var Basketball = function() { this.intro = '籃球盛行於美國'; }; Basketball.prototype = { getMember: function() { console.log('每一個隊伍須要5名隊員'); }, getBallSize: function() { console.log('籃球很大'); } }; // 足球基類 var Football = function() { this.intro = '足球盛行於美國'; }; Football.prototype = { getMember: function() { console.log('每一個隊伍須要11名隊員'); }, getBallSize: function() { console.log('籃球很大'); } }; // 運動工廠 var SportsFactory = function(name) { switch(name) { case 'NBA': return new Basketball(); case 'wordCup': return new Football(); } };
當你使用這個運動工廠時只須要記住SportsFactory這個工廠對象就行了,它會幫你找到你想要的。
簡單工廠模式的理念是建立對象,上面例子是將不一樣的類實例化,可是簡單工廠模式還能夠建立類似對象,將類似的東西提取,不類似的針對性處理便可。這樣只需建立一個對象就能夠替代多個類了。java
團隊開發不一樣於我的,對全局變量的限制很大,要儘可能少得建立全局變量。若是有同一類對象在不一樣需求中重複使用,那麼大部分是不須要重複建立的,要學會代碼複用。用簡單工廠來建立對象,能夠減小全局變量建立提升代碼複用率,它的使用場合限制在建立單一對象。ajax
做者把工廠方法模式比喻成一張名片。算法
經過對產品類的抽象使其建立業務主要負責用於建立多類產品的實例。編程
在實際開發中,需求的變動是很正常的,開始需求簡單能夠直接建立對象,相似的需求多了能夠用簡單工廠方法重構,可是若是需求不停變化,那麼不只要修改工廠函數還要添加類,這樣就沒完了。而工廠方法模式本意是將實際建立對象的工做推遲到子類中。
// 工廠類 var Factory = function(type, content) { if(this instanceof Factory) { var s = new this[type](content); return s; } else { // 防止使用者不知道這是一個類,忘了加new操做符建立,致使全局變量污染 return new Factory(type, content); } }; Factory.prototype = { Java: function(content) { // ... }, JavaScript: function(content) { // ... }, php: function(content) { // ... } };
這樣之後若是想添加其餘類,只須要在Factory的原型裏添加就能夠了。
對於建立不少類的對象,簡單工廠模式就不適合了,經過工廠模式能夠輕鬆建立多個類的實例對象,並且避免了使用者與對象類之間的耦合,用戶沒必要關心建立該對象的具體類,只需調用工廠方法便可。
抽象工廠模式讓你感受出現的都是幻覺。
經過對類的工廠抽象使其業務用於對產品類簇的建立,而不負責某一類產品的實例。
抽象類是一種聲明但不能使用的類,當你使用的時候就會報錯。JavaScript中的抽象類不能像傳統面嚮對象語言那樣輕鬆地建立,咱們能夠在類的方法中手動拋出錯誤來模擬抽象類。你可能會想,這樣的類什麼都不能作能有什麼用?其實它在繼承上是頗有用的。
抽象工廠模式不能用來建立具體對象,通常用它做爲父類類建立一些子類。
// 抽象工廠方法 var VehicleFactory = function(subType, superType) { // 判斷抽象工廠中是否有該抽象類 if(typeof VehicleFactory[superType] === 'function') { // 緩存類 function F() {}; // 繼承父類屬性和方法 F.prototype = new VehicleFactory[superType](); // 將子類構造函數指向子類 subType.constructor = subType; // 子類原型繼承父類 subType.prototype = new F(); } else { // 不存在該抽象類拋出錯誤 throw new Error('未建立該抽象類'); } }; // 小汽車抽象類 VehicleFactory.Car = function() { this.type = 'car'; }; VehicleFactory.Car.prototype = { getPrice: function() { return new Error('抽象方法不能調用') } }; // 公交車抽象類 VehicleFactory.Bus = function() { this.type = 'bus'; }; VehicleFactory.Bus.prototype = { getPrice: function() { return new Error('抽象方法不能調用'); } };
抽象工廠其實是一個子類繼承父類的方法,在該方法中須要經過傳遞子類以及繼承父類的名稱。
抽象工廠模式是設計模式中最抽象的一種,也是建立模式中惟一一種抽象化建立模式。該模式建立出的結果不是一個真實的對象實例,而是一個類簇,指定了類的結構。
建造者模式告訴咱們分便是合。
將一個複雜對象的構建層與其表示層相互分離,一樣的構建過程可採用不一樣的表示。
如今有一個發佈簡歷的需求,就是幫別人在公司網站上發佈簡歷,可是這些簡歷有一個需求,除了將興趣愛好以及一些特長髮布在頁面裏,其餘信息如聯繫方式等不要發佈在網站上,並且每一個人想找的工做是能夠分類的。這樣一些需求咱們須要建立的東西就多了,這時候前面的三種工廠模式都不適合了,這裏就能夠用建造者模式。
建造者模式和只關心建立結果的工廠模式不一樣,雖然其目的也是建立一個對象,可是更多關心的是建立這個對象的整個過程。在本例中,咱們須要的不只僅是應聘者的實例還要在建立過程當中注意這位應聘者有哪些興趣愛好等。
// 建立一位人類 var Human = function(param) { // 技能 this.skill = param && param.skill || '保密'; // 興趣愛好 this.hobby = param && param.hobby || '保密'; }; // 類人原型方法 Human.prototype = { getSkill: function() { return this.skill; }, getHobby: function() { return this.hobby; } }; // 實例化姓名類 var Named = function(name) { var that = this; // 構造器,解析姓名的姓與名 (function(name, that) { that.wholeName = name; if(name.indexOf(' ') > -1) { that.FirstName = name.slice(0, name.indexOf(' ')); that.FirstName = name.slice(name.indexOf(' ')); } })(name, that); }; // 實例化職位類 var Work = function(work) { var that = this; // 構造器,經過傳入的職位特徵來設置相應職位及描述 (function(work, that) { switch(work) { case 'code': that.work = '工程師'; break; case 'UI': case 'UE': that.work = '設計師'; break; case 'teach': that.work = '教師'; break; default: that.work = work; } })(work, that); }; // 更換指望的職位 Work.prototype.changeWork = function(work) { this.work = work; };
下面來建立一位應聘者
// 應聘者建立類 var Person = function(name, work) { // 建立應聘者緩存對象 var _person = new Human(); // 建立應聘者姓名解析對象 _person.name = new Named(name); // 建立應聘者指望職位 _person.work = new Work(work); // 返回建立的應聘者對象 return _person; }
建造者模式和前面幾種建立型設計模式不一樣,它關心對象的整個建立過程,所以一般將建立對象的類模塊化,這樣使建立類的每個模塊均可以獲得靈活的運用與高質量的複用。這種方式對於整個對象類的拆分無形中增長告終構的複雜性,所以若是對象粒度很小,或者模塊間的複用率很低,不建議使用建造者模式。
原型模式是JavaScript語言之魂。
用原型實例指向建立對象的類,使用於建立新的對象的類共享原型對象的屬性以及方法。
仍是關於子類繼承父類的問題,爲了提升性能,對於每次建立的一些簡單的而又有差別化的屬性能夠放在構造函數中,將一些消耗資源比較大的方法放在基類的原型中,這樣就能夠避免沒必要要的消耗,這就是原型模式的雛形。
原型模式更多的是用在對象的建立上,好比建立一個實例對象的構造函數比較複雜或者耗時比較長,或者經過建立多個對象來實現。此時最好不要用new關鍵字去複製這些基類,能夠經過對這些對象屬性或者方法進行復制來實現建立。首先要有一個原型對象的複製方法。
// 原型對象複製方法 function prototypeExtend() { var F = function() {}, args = arguments, i = 0, len = args.length; for (; i < len; i++) { // 遍歷每一個模板對象中的屬性 for(var j in args[i]) { F.prototype[j] = args[i][j]; } } // 返回緩存類實例 return new F(); }
企鵝遊戲中建立一個企鵝對象,若是沒有企鵝基類,只提供了一些動做模板對象,能夠經過實現這些模板對象的繼承來建立一個企鵝實例對象。
var penguin = prototypeExtend({ speed: 20, swim: function() { console.log('游泳速度' + this.speed); }, run: function() { console.log('奔跑速度' + this.speed); } })
這樣經過prototypeExtend建立的就是一個對象,不用再用new去建立一個新的實例對象。
原型模式實際上也是一種繼承,可讓多個對象分享同一個原型對象的屬性和方法,這種繼承的實現是不須要建立的,而是將原型對象分享給那些繼承的對象。原型對象更適合在建立複雜的對象時,對於那些需求一直在變化而致使對象結構不停地改變時,將那些比較穩定的屬性與方法共用而提取的繼承的實現。
哈哈,讓你感覺下一我的的寂寞。
又被稱爲單體模式,只容許實例化一次的對象類。有時也能夠用一個對象來規劃一個命名空間,層次分明地管理對象上的屬性和方法。
單例模式應該是JavaScript中最多見的一種設計模式了,常常爲咱們提供一個命名空間,來防止不一樣的人命名變量的衝突。還能夠用它來建立一個小型的代碼庫。
var A = { Util: { util_method1: function() {}, util_method2: function() {} }, Tool: { tool_method1: function() {}, tool_method2: function() {} }, Ajax: { ajax_method1: function() {}, ajax_method2: function() {} } ... }
若是想使用這個代碼庫,像下面這樣訪問便可:
A.Util.util_method1(); A.Tool.tool_method2();
單例模式有時也被稱爲單體模式,它是隻容許實例化一次的對象類,有時這麼作也是爲了節省系統資源。JavaScript中單例模式常常做爲命名空間對象來實現,經過單例對象,咱們能夠將各個模塊的代碼層次分明地梳理在一塊兒。
結構型設計模式關注於如何將類或對象組合成更大、更復雜的結構,以簡化設計。主要有外觀模式,適配器模式,代理模式,裝飾者模式,橋接模式,組合模式和享元模式。
做者把這種模式比喻成一種套餐服務。
爲一組複雜的子系統接口提供一個更高級的統一接口,經過這個接口使得對子系統接口的訪問更加容易。在JavaScript中有時也會用於對底層結構兼容性作統一封裝來簡化用戶使用。
爲頁面文檔document對象添加點擊事件時,若是直接用onclick來綁定事件,那麼若是團隊中再有人要爲document綁定click事件時,就會把以前綁定的那個時間覆蓋,由於這是DOM0級事件。咱們應該用DOM2級事件處理程序提供的addEventListener來實現,然而老版本IE是不支持這個方法的,必須用attachEvent,這樣若是咱們寫一個能兼容全部瀏覽器的方式操做起來就會更方便,這時候就能夠用到外觀模式。爲功能統一但方法不統一的接口提供一個統一的接口。
// 外觀模式實現 function addEvent(dom, type, fn) { // 對於支持DOM2級事件處理程序的瀏覽器 if(dom.addEventListener) { dom.addEventListener(type, fn, false); // 對於不支持addEventListener但支持attachEvent的瀏覽器 } else if(dom.attachEvent) { dom.attachEvent('on' + type, fn); } else { dom['on' + type] = fn; } }
解決瀏覽器兼容問題只是外觀模式應用的一部分,不少代碼庫中都是經過外觀模式來封裝多個功能,簡化底層造做方法的。
當一個複雜的系統提供一系列複雜的接口方法時,爲系統的管理方便會形成接口方法的使用及其複雜。經過外觀模式,對接口進行二次封裝能夠隱藏其複雜性。
聽到這個是的名字,有沒有想到水管彎彎的場景呢?
將一個類(對象)的接口(方法或者屬性)轉化成另一個接口,以知足用戶需求,使類(對象)之間接口的不兼容問題經過適配器得以解決。
公司有個活動頁面正在使用公司內部開發的A框架,但是不少新來的同事使用A框架開發新的功能需求時老是感受很吃力,並且能用的方法有限,爲了讓新同事儘快融入項目的開發,能夠引入jQuery框架,因爲A框架和jQuery框架很像,這樣就能夠寫一個適配器而不須要將以前的代碼全用jQuery寫一遍。
適配器模式不只在編程中很常見,在生活中這種模式也很常見,好比三角插頭充電器對於兩項插頭是不能用的,此時就須要一個三項轉兩項插頭電源適配器,這就是一種適配器模式,其實它就是爲了兩個代碼庫所寫的代碼兼容運行而書寫的額外代碼。
JavaScript中適配器模式還能適配兩個代碼庫,適配參數,適配數據,適配服務端數據等。以參數適配爲例。
function doSomeThing(name, title, age, color, size, prize){}
記住這些參數的順序是很困難的,因此咱們常常是以一個參數對象方式傳入的,以下所示:
/** * obj.name: name * obj.title: title * obj.age: age * obj.color: color * obj.size: size * obj.prize: prize ***/ function doSomeThing(obj){}
然而當調用的時候也不能肯定傳遞的參數是否完整,若有一些必須得參數沒有傳入,一些參數有默認值等,這個時候就能夠用適配器來適配傳入的參數對象。
function doSomeThing(obj) { var _adapter = { name: '雨夜清荷', title: '設計模式', age: 24, color: 'pink', size: 100, prize: 50 }; for(var i in _adapter) { _adapter[i] = obj[i] || _adapter[i]; } }
JavaScript中的適配器更多應用在對象之間,爲了使對象可用,一般會將對象拆分並從新包裝,這樣就要了解適配器對象的內部結構,這也是與外觀模式的區別所在。
有沒有想到牛郎織女鵲橋相會的場景?
因爲一個對象不能直接引用另外一個對象,因此須要經過代理對象在這兩個對象之間起到中介做用。
跨域問題應該是使用代理模式解決的一個最典型的問題。因爲用戶模塊上傳的照片量愈來愈大,致使服務器須要將上傳模塊從新部署到另一個域中,這就致使了跨域問題。咱們能夠將相冊頁面和上傳模塊所在的服務器抽象成兩個對象,想讓跨域兩端的對象之間實現通訊,就須要找個代理對象來實現他們之間的通訊。
代理對象有不少種,簡單一點的如img之類的標籤經過src能夠向其餘域下的服務器發送請求。不過這類請求是get請求,是單向的,不會有響應數據。另一種代理對象的形式是經過script標籤。而咱們須要的代理對象,是對頁面與瀏覽器間通訊的,JSONP就實現了一種代理模式。咱們知道src屬性能夠實現get請求,所以能夠在src指向的url地址上添加一些字段信息,服務器獲取這些字段信息,相應生成一份內容。
// 前端瀏覽器頁面 <script type="text/javascript"> // 回調函數 function jsonpCallBack(res,req) { console.log(res,req); } </script> <script type="text/javascript" src="http://localhost/test/jsonp.php?callback=jsonp CallBack&data=getJsonPData"></script>
// 另外一個域下的服務器請求接口 <?php /* 後端獲取請求字段數據,並生成返回內容 */ $data = $_GET["data"]; $callback = $_GET["callback"]; echo $callback."('success', '".$data."')"; ?>
這種方式能夠想象成合理的一隻小船,經過小船將你的請求發送給對岸,而後對岸的人們將數據放在小船裏爲你帶回來。
代理模式除了在跨域問題中有不少應用外,有時對對象的實例化對資源的開銷很大,如頁面加載初期加載文件有不少,此時可以延遲加載一些圖片對頁面首屏加載時間收益是很大的,經過代理能夠先加載預覽圖片而後再加載開銷大的圖片。
因而可知,代理模式能夠解決系統之間耦合度以及系統資源開銷大的問題,經過代理對象能夠保護被代理對象,使被代理對象不受外界的影響。
顯然房子裝修就是一種典型的裝飾者模式。
在不改變原對象的基礎上,經過對其進行包裝擴展(添加屬性或者方法)使原有對象能夠知足用戶的更復雜需求。
靜止是相對的,運動是絕對的,因此沒有一成不變的需求。在實際項目開發中需求總在不斷變化,當原有的功能已經不能知足用戶的需求時,咱們要作的就是在這個基礎上添磚加瓦,設置新功能和屬性來知足用戶提出的需求,這就是裝飾者模式要作的。
// 裝飾者 var decorator = function(input, fn) { // 獲取事件源 var input = document.getElementById(input); // 若事件源已經綁定事件 if(typeof input.onclick === 'function') { // 緩存事件源原有回調函數 var oldClickFn = input.onclick; // 爲事件源定義新的事件 input.onclick = function() { // 事件源原有回調函數 oldClickFn(); // 執行事件源新增回調函數 fn(); } } else { input.onclick = fn; } }
除了裝飾者模式,適配器模式也能夠對原有對象進行擴展,所不一樣的是適配器進行擴展不少時候是對對象內部結構的重組,所以瞭解其自身結構是必須的。而裝飾者模式對對象的擴展是一種良性擴展,不用瞭解其具體實現,只是在外部進行了一次封裝擴展。
做者把這種模式比喻成城市間的公路。
在系統沿着多個維度變化的同時,又不增長其複雜度並已達到解耦。
有時候,頁面中一些小小細節的改變經常因邏輯類似而致使大片臃腫的代碼,讓頁面苦澀不堪。如今項目有一個需求,是要把頁面上部的用戶信息添加一些鼠標劃過的特效,可是用戶信息由不少小組件組成,對於用戶名,鼠標劃過直接改變背景色,可是像用戶等級、用戶消息這類部件只能改變裏面的數字內容,處理邏輯不太同樣。這樣就須要寫很多代碼,可是又會感受很冗餘。這時候,咱們首先要提取共同點,對想的抽象邏輯作抽象提取處理。
對於用戶信息模塊的每一部分鼠標滑過與鼠標離開兩個事件的執行函數有很大一部分是類似的,好比它們都處理每一個部件中的某個元素,它們都是處理元素的字體顏色和背景顏色。能夠建立下面這樣一個函數,解除this耦合。
function changeColor(dom, color, bg) { // 設置元素的字體顏色 dom.style.color = color; // 設置元素的背景顏色 dom.style.background = bg; }
接下來就是對具體元素綁定時間了,可是僅僅知道元素事件綁定與抽象提取的設置樣式方法changeColor是不夠的,須要用一個方法將他們連接起來,這個方法就是橋接方法,這種模式就是橋接模式。就像你開着車去瀋陽,那麼你就須要找到一條鏈接北京與瀋陽的公路,才能順利往返兩地。
對於事件的橋接方法,能夠用一個匿名函數來代替。
var spans = document.getElementsByTagName('span'); spans[0].onmouseover = function() { changeColor(this, 'red', '#ddd'); }
橋接模式最主要的特色是將實現層(如元素綁定事件)與抽象層(如修飾頁面UI邏輯)解耦分離,使兩部分能夠獨立變化,橋接模式主要是對結構之間的解耦。
做者把組合模式比喻成超值午飯,感受很形象。
又稱部分-總體模式,將對象組合成樹形結構以表示「部分總體」的層級結構。組合模式使得用戶對單個對象和組合對象的使用具備一致性。
爲強化首頁用戶體驗,項目經理準備在用戶首頁添加一個新聞模塊,固然新聞的內容是根據用戶平時關注的內容挖掘的,所以有的人可能會顯示文字新聞,有的人可能會是圖片新聞等等。
咱們先來仔細分析下這個需求,需求中的這些新聞大體能夠分爲相互獨立的幾種類型,對某類新聞作修改時不會影響到其餘類的新聞,這樣能夠將每一類新聞抽象成面向對象編程中的一個類,而後在這些新聞類中挑選一些組合成須要的模塊,這時候就能夠用組合模式了。
在頁面中,組合模式更經常使用在建立表單上,好比註冊頁面可能有不一樣的表單提交模塊。對於這些需求,咱們只須要有一個基本的個體,而後經過必定的組合便可實現。
組合模式可以給咱們提供一個清晰的組成結構,組合對象類經過繼承同一個父類使其具備統一的方法,這樣也方便了統一管理與使用。
做者把享元模式比喻成城市公交車,能夠仔細思考一番。
運用共享技術有效地支持大量的細粒度的對象,避免對象間擁有相同內容形成多餘的開銷。
如今有新聞的內容太多,咱們有了一個分頁顯示全部新聞的需求。一個簡單直觀的作法就是頁面加載後異步請求新聞數據,而後建立全部條新聞插入頁面中,須要顯示哪一頁就顯示哪一頁。可是這樣作有一個很大的問題,這樣一會兒建立幾百條新聞同時插入頁面會形成多頁的開銷嚴重影響網頁的性能。這裏的全部新聞都有類似的結構,只是內容不一樣罷了,對於這種相同結構形成多餘開銷的問題,能夠用享元模式來解決。
享元模式 主要是對其數據、方法共享分離,將數據和方法分紅內部數據、內部方法和外部數據、外部方法。內部方法與內部數據指的是類似或共有的數據和方法,因此將其提取出來減小開銷。上面例子中,全部新聞個體都有共同的結構,應該做爲內部數據,而下一頁按鈕綁定的事件則是外部方法。同時爲了使用內部數據還須要提供一個操做方法。
var Flyweight = function() { // 已建立的元素 var created = []; // 建立一個新聞包裝容器 function create() { var dom = document.createElement('div'); // 將容器插入新聞列表容器中 document.getElementById('container').appendChild(dom); // 緩存新建立的元素 created.push(dom); // 返回建立的新元素 return dom; } return { // 獲取建立新聞元素方法 getDiv: function() { // 若是已建立的元素小於當前頁元素總個數(5個),則建立 if(created.length < 5) { return created(); } else { // 獲取第一個元素,並插入去後面 var div = created.shift(); created.push(div); return div; } } } }
上面建立一個享元類,因爲每頁只能顯示5條新聞,因此建立5個元素,保存在享元類內部,能夠經過getDiv方法來獲取建立的元素。下面就要實現外部數據和外部方法,外部數據就是咱們要顯示的全部新聞內容,因爲每一個內容都不同確定不能共享。首先,咱們要根據新聞內容實例化頁面,而後,對下一頁綁定一個點擊事件,顯示下一頁。
var paper = 0, num = 5, len = article.length; // 添加五條新聞 for(var i = 0; i < 5; i++) { if(article[i]) // 經過享元類獲取建立的元素並寫入新聞內容 Flyweight.getDiv().innerHTML = article[i]; }
// 下一頁按鈕綁定事件 document.getElementById('next_page').onclick = function() { // 若是新聞內容不足5條則返回 if(article.length < 5) { return; } var n = ++paper * num % len, // 獲取當前頁的第一條新聞索引 j = 0; // 插入5條新聞 for(; j < 5; j++) { // 若是存在n+j條則插入 if(article[n + j]) { Flyweight.getDiv().innerHTML = article[n + j]; // 不然插入起始位置第n+j-len條 } else if(article[n + j - len]) { Flyweight.getDiv().innerHTML = article[n + j - len]; } else { Flyweight.getDiv().innerHTML = ""; } } }
這樣用享元模式對頁面重構以後每次操做只須要操做5個元素,這樣性能能夠提升不少。
享元模式的應用是爲了提升程序的執行效率與系統性能,所以在大型系統開發中應用比較普遍,能夠避免程序中的數據重複。應用時必定要找準內部狀態與外部狀態,這樣才能更合理地提取分離。
行爲型設計模式用於不一樣對象之間職責劃分或算法抽象,行爲型設計模式不只僅涉及類和對象,還涉及類或對象之間的交流模式並加以實現。行爲型設計模式主要有模板方法模式,觀察者模式,狀態模式,策略模式,職責鏈模式,命令模式,訪問者模式,中介者模式,備忘錄模式,迭代器模式和解釋器模式,這麼多的模式真得好好消化一陣子了。
做者把這種模式比喻成照貓畫虎。
父類中定義一組操做算法骨架,而將一些實現步驟延遲到子類,使得子類能夠不改變父類算法結構的同時可從新定義算法中某些實現步驟。
提示框歸一化,一個網站有不少頁面,若是每一個頁面的彈出框樣式不太一致就會顯得不是很和諧,須要將他們的樣式統一。新手最直觀的想法就是去每一個頁面一個個修改,固然這樣的代價是很大的,咱們須要寫一個彈出框插件,將這些彈出框封裝好,而後再各個頁面調用便可。這是在這個插件中就可使用模板方法模式了,不須要重複寫多個樣式。
模板方法模式就是將多個模型抽象畫歸一,從中抽象出一個最基本的模板,這個模板能夠做爲實體也能夠做爲抽象對象,其餘模塊只須要繼承這個模板方法,也能夠擴展某些方法。
打個比方,咱們生活中用蛋糕作模具作蛋糕,作出的蛋糕是外形相同的,由於他們都用同一個模具。然而商店裏面賣的蛋糕是各式各樣的,這都是對蛋糕的二次加工。咱們的需求中基本提示框就是咱們抽象出來的模具,其餘提示框比這個提示框要多一些功能,咱們只須要對他們作一些二次加工就能知足需求了。
模板方法不只在歸一化組件時使用,有時候建立頁面時也是很經常使用的,好比建立三類導航,第一類是基礎的,第二類是多了消息提醒功能的,第三類多了後面顯示網址功能。這也能夠用模板方法實現,此時抽象出來的基類是最簡單的基礎導航類。
// 格式化字符串方法 function formateString(str, data) { return str.replace(/\{#(\w+)#\}/g, function(match, key) { return typeof data[key] === undefined ? '': data[key] }); } // 基礎導航 var Nav = function(data) { // 基礎導航樣式模板 this.item = '<a href="{#href#}" title="{#title#}">{#name#}</a>'; // 建立字符串 this.html = ''; // 格式化數據 for(var i = 0, len = data.length; i < len; i++) { this.html += formateString(this.item, data[i]); } // 返回字符串數據 return this.html; }
對於消息提醒導航類,只需額外添加消息提醒組件模板,並與消息提醒組件模板對傳入的網址數據進行裝飾,獲得所需的字符串,在調用從基類繼承的方法處理這些字符串便可。
var NumNav = function(data) { // 消息提醒信息組件模板 var tpl = '<b>{#num#}</b>'; // 裝飾數據 for(var i = data.length - 1; i >= 0; i--) { data[i].name += data[i].name + formateString(tpl, data[i]); } // 繼承基礎導航類 return Nav.call(this, data); }
模板方法的核心在於對方法的重用,將核心方法封裝在基類中,讓子類繼承基類的方法,實現基類方法的共享,達到方法共用。子類繼承的方法是可擴展的,這就須要對基類繼承的方法進行重寫。
做者把這種模式比喻成通訊衛星。
又被稱做發佈-訂閱模式或消息機制,定義了一種依賴關係,解決了主體對象與觀察者之間功能的耦合。
在團隊開發中,常常是一我的負責一個模塊,那麼每人負責的模塊之間要如何進行溝通呢?好比你實現一些需求須要添加一些代碼,可是這個需求須要其餘模塊配合,可是每一個模塊都是不一樣人寫的,你不想由於新添加的代碼影響到他人實現的功能,這個時候就須要用到觀察者模式了。
觀察者模式就是爲了解決主體對象與觀察者之間的耦合。打個比方,目前每一個國家都在研發併發射衛星,發射這些衛星是爲了監控一些信息,那麼它就能夠被看作一個觀察者或者說是一個消息系統,若是讓這顆衛星爲飛機導航,那麼這架飛機就是一個被觀察者或者說是一個主體對象。那麼若是地面上的中轉站或者其餘飛機須要知道這架飛機的信息,因而每當飛機到達一個地方時就會向衛星發出位子信息,而後衛星又將信息廣播到已經訂閱這架飛機的中轉站,這樣就能夠避免一些飛機事故發生。
這時候,觀察者至少須要有兩個方法,一個是接收某架飛機發來的消息,一個是向訂閱的中轉站發送響應消息。可是,並非每一箇中轉站都要時刻監控飛機狀態的,因此還須要一個取消註冊的方法。固然這些消息還須要保存,就須要一個保存消息的容器。這時候觀察者雛形就出來了,他有一個消息容器和三個方法,訂閱消息方法,取消訂閱消息方法,發送訂閱消息方法。
var Observer = (function() { // 防止消息隊列暴露而被篡改,故將消息容器做爲靜態私有變量保存 var __messages = {}; return { // 註冊信息接口 regist: function() {}, // 發佈信息接口 fire: function() {}, // 移除信息接口 remove: function() {} } })();
下面就是能夠本身具體實現這些接口了。
觀察者模式最主要是解決類或對象之間的耦合,解耦兩個互相依賴的對象,使其依賴於觀察者的消息機制。這樣對於任何一個訂閱者來講,其餘訂閱者對象的改變不會影響到自身,其自身既能夠是消息的發出者也能夠是消息的執行者,這都依賴於調用觀察者對象中的三種方法(訂閱,註銷,發佈消息)中的哪種。
做者把這種模式比喻成超級瑪麗。
當一個對象內部狀態發生改變時,會致使其行爲的改變,這看起來像是改變了對像。
平時寫代碼的時候常常會遇到要寫不少條件判斷語句的狀況,那麼怎麼減小代碼中的條件判斷語句呢?對於這類分支條件內部獨立結果的管理,可使用狀態模式,每一種條件做爲對象的一種狀態,面對不一樣的判斷結果,其實就是選擇對象內的一種狀態。
將不一樣的判斷結果封裝在狀態對象內,而後該狀態對象返回一個可被調用的接口方法,用於調用狀態對象內部的某種方法。
// 投票結果狀態對象 var ResultState = function() { // 判斷結果保存在內部狀態中 var States = { // 每種狀態做爲一種獨立方法保存 state0: function() { console.log('這是第一種狀況'): }, state1: function() { console.log('這是第二種狀況'): }, state2: function() { console.log('這是第三種狀況'): }, state3: function() { console.log('這是第四種狀況'): } } // 獲取某種狀態並執行對應方法 function show(result) { States['state' + result] && States['state' + result](); } return { // 返回調用狀態方法接口 show: show } }();
想調用第三種結果就能夠以下調用
ResultState.show(3);
對於狀態模式,主要目的就是將條件判斷的不一樣結果轉化爲狀態對象的內部狀態,這個內部狀態通常做爲狀態對象的私有變量,而後提供一個可以調用狀態對象內部狀態的接口方法對象便可。
狀態模式既是解決程序中臃腫的分支判斷語句問題,將每個分支轉化爲一種狀態獨立出來,方便每種狀態的管理又不至於每次只需時遍歷全部分支。
做者把這種模式比喻成活諸葛。
將定義的一組算法封裝起來,使其相互之間能夠替換。封裝的算法具備必定獨立性,不會隨客戶端變化而變化。
年末的時候,公司商品展銷頁都要開展大促銷活動。在聖誕節,一部分商品5折出售,一部分商品8折出售,一部分商品9折出售,到元旦搞個幸運反饋活動,普通用戶滿100返30,高級VIP用戶滿100返50。這個時候上面的狀態模式就不適用了,由於每一天每個商品只有一種促銷狀況,這個時候能夠用策略模式。
結構上看,它與狀態模式很像,也是在內部封裝一個對象,而後經過返回的接口對象實現實現對內部對象的調用,不一樣點是,策略模式不須要管理狀態、狀態間沒有依賴關係、策略之劍能夠相互替換、在策略對象內部保存的是相互獨立的一些算法。看看策略對象的實現:
// 價格策略對象 var PriceStrategy = function() { // 內部算法對象 var strategy = { // 100返30 return30: function(price) {}, // 100返50 return50: function(price) {}, // 9折 percent90: function(price) {}, // 8折 percent80: function(price) {}, // 5折 percent50: function(price) {}, } // 策略算法調用接口 return function(algorithm, price) { return strategy[algorithm] && strategy[algorithm](price); } }();
策略模式主要特點是建立一系列策略算法,每組算法處理業務都是相同的,只是處理的過程或者處理的結果不同,因此它們是能夠相互替換的,這樣就解決了算法與使用者之間的耦合。
做者把這種模式比喻成一個有序車站。
解決請求的發送者與請求的接受者之間的耦合,經過職責鏈上的多個對象對分解請求流程,實現請求在多個對象之間的傳遞,知道最後一個對象完成請求的處理。
項目經理準備改善頁面中的輸入驗證與提示交互體驗。如用戶在輸入框輸入信息後,在輸入框的下面提示出一些備選項,當用戶輸入完成後,則要對用戶輸入信息進行驗證等,頁面中不少模塊須要用戶提交信息,爲加強用戶體驗,這些輸入框大部分須要具有以上兩種功能。如今須要完成這個需求,可是之後可能要對原有表單交互體驗作一些修改,也就是這是一個半成品需求。這種狀況下,咱們須要將需求裏面須要作的每一件事情獨立出來,這樣完整的需求就變成一個個相互獨立的模塊需求,這樣就不會由於之後需求的改變而影響咱們項目的進展,這樣還有利於之後的單元測試。這其實就是一種職責鏈模式。
對於上面的需求,對輸入框綁定事件是第一部分,第二部分是建立xhr進行異步數據獲取,第三部分就是適配響應數據,將接收到的數據格式化成可處理的形式,最後一部分是向組件建立器傳入相應數據生成組件。
職責鏈模式定義了請求的傳遞方向,經過多個對象對請求的傳遞,實現一個複雜的邏輯操做。所以職責鏈模式將負責的需求顆粒化逐一實現每一個最小份內的需求,並將請求順序地傳遞。對於職責鏈上的每個對象來講,它多是請求的發起者也多是請求的接收者,經過這種方式不只僅簡化原對象的複雜度,並且解決原請求的發起者與原請求的接收者之間的耦合。
將請求與實現解耦並封裝成獨立對象,從而使不一樣的請求對客戶端的實現參數化。
如今的需求是要作一個活動頁面,平鋪式的結構,不過頁面的每一個模塊都有些類似的地方,好比每一個預覽產品圖片區域,都有一行標題,而後標題下面是產品圖片,只是圖片的數量與排列不一樣。咱們須要一種自由建立視圖模塊的方法,有時候建立多張圖片有時候只建立一張圖片,這時候能夠試試命令模式。
命令模式是將建立模塊的邏輯封裝在一個對象裏,這個對象提供一個參數化的請求接口,經過調用這個接口並傳遞一些參數實現調用命令對象內部中的一些方法。請求部分很簡單,只須要按照給定參數格式書寫指令便可,因此實現部分的封裝纔是重點,由於它要爲請求部分提供所需方法。
那麼哪些對象須要被命令化呢?既然須要動態展現不一樣模塊,因此建立元素這一需求就是變化的,所以建立元素方法、展現方法應該被命令化。
// 模塊實現模塊 var viewCommand = (function() { var tpl = { // 展現圖片結構模塊 product: [ '<div>',.....,'</div>' ].join(''), // 展現標題結構模塊 title: [ '<div>',.....,'</div>' ].join(''), }, // 格式化字符串緩存字符串 html = ''; // 格式化字符串 function formateString(str, obj) {} // 方法集合 var Action = { // 建立方法 create: function(data, view) { // 解析數據 if(data.length) { // 遍歷 for(var i = 0, len = data.length; i < len; i++) { html += formateString(tpl[view], data[i]); } } else { html += formateString(tpl[view], data); } }, // 展現方法 display: function(container, data, vuew) { // 若是傳入數據 if(data) { // 根據給的數據建立視圖 this.create(data, view); } // 展現模塊 document.getElementById(container).innerHTML = html; // 展現後清空緩存字符串 html = ''; } } // 命令接口 return function excute(msg) { // 解析命令,若是msg.param不是數組則將其轉化爲數組 msg.param = Object.prototype.toString.call(msg.param) === "[object Array]" ? msg.param : [msg.param]; // Action內部調用的方法引用this,此處保證做用域this執行傳入Action Action[msg.command].apply(Action, msg.param) } })();
下面就能夠測試這個命令對象了:
var productData = [ { src: 'command/02.jpg', text: '綻開的桃花' }, { src: 'command/03.jpg', text: '陽光下的舒適' } ], // 模塊標題數據 titleData = { title: '夏日裏的一片舒適', tips: '暖暖的溫情帶給人們家的感受' } // 調用命令對象 viewCommand({ command: 'display', param: ['title', titleData, 'title'] }); viewCommand({ command: 'create', param: ['product', productData, 'product'] });
有了命令模式,想建立任何頁面視圖都是一件很簡單的事情。
命令模式是將執行的命令封裝,解決命令發起者與命令執行者之間的耦合,每一條命令實質上是一個操做。命令的是使用者沒必要了解命令執行者的命令接口是如何實現的,只須要知道如何調用。
做者把這種模式比喻成駐華大使。
針對於對象結構中的元素,定義在不改變對象的前提下訪問結構中元素的新方法。
用DOM2級事件爲頁面中元素綁定事件時,爲css設置一些樣式以下:
var bindEvent = function(dom, type, fn) { if(dom.addEventListener) { dom.addEventListener(type, fn, false); } else if(dom.attachEvent) { dom.attachEvent('on' + type, fn); } else { dom['on' + type] = fn; } } var demo = document.getElementById('demo'); bindEvent(demo, 'click', function() { this.style.background = 'red'; });
這個在IE瀏覽器中會出問題,由於IE的attachEvent事件中this指向的居然是window而不是這個元素,因此若是想獲取事件對象必須用window.e來獲取。這個問題能夠借用訪問者模式來解決。
訪問者模式的思想是咱們在不改變操做對象的同時,爲它添加新的操做方法,來實現對操做對象的訪問。下面看看IE的實現方式:
function bindIEEvent(dom, type, fn, data) { var data = data || {}; dom.attachEvent('on' + type, function(e){ fn.call(dom, e, data); }); };
上面實現方法的核心就是調用call方法,call方法的做用就是更改函數執行時的做用域,這正是訪問者模式的精髓。
訪問者模式解決數據與數據操做方法之間的耦合,將數據的操做方法獨立於數據,使其能夠自由化演變。訪問者更適合那些數據穩定可是數據的操做方法易變的環境下。
做者把這種模式比喻成媒婆,好吧,我笑了這裏。
經過中介者對象封裝一系列對象之間的交互,是對象之間再也不相互引用,下降他們之間的耦合。有時中介者對象也能夠改變對象之間的交互。
項目經理準備在用戶首頁上的導航模塊添加一個設置層,讓用戶能夠經過設置層來設置導航展開樣式。可是頁面中好多模塊都有導航,這要改起來工做量也很大,上面講的觀察者模式雖然能解決模塊之間的耦合,可是這裏咱們並無須要向設置層發送請求的需求,設置層只是單向控制導航模塊內導航的樣式。這樣的單向通訊就可使用中介者模式。
觀察者模式和中介者模式都是經過消息收發機制實現,不過在觀察者模式中,一個對象既能夠是消息的發送者也能夠是消息的接收者,而中介者模式中消息的發送方只有一個就是中介者對象,並且中介者對象不能訂閱消息,只有那些活躍對象(訂閱者)才能訂閱中介者消息。
若是用中介者模式來解決上面的問題,那麼中介者對象就是設置層模塊對象,它負責向各個導航模塊對象發送用戶設置消息,而各個導航模塊則應該做爲消息的訂閱者存在,實現以下:
// 中介者對象 var Mediator = function() { // 消息對象 var _msg = {}; return { // 訂閱消息方法,type:消息名稱 action:消息回調函數 register: function(type, action) { // 若是消息存在 if(_msg[type]) // 存入回調函數 _msg[type].push(action); else { // 不存在則創建消息容器 _msg[type] = []; _msg[type].push(action); } }, // 發佈消息方法 send: function(type) { // 若是該消息已經被訂閱 if(_msg[type]) { // 遍歷已存儲的消息回調函數 for(var i = 0, len = _msg[type].length; i < len; i++) { // 執行回調函數 _msg[type][i] && _msg[type][i](); } } } } }();
這樣就建立了一箇中介者對象,下面就能夠利用這個中介者對象完成咱們的需求了。
同觀察者模式同樣,中介者模式的主要業務也是經過模塊間或者對象間的複雜通訊,來解決模塊間或對象間的耦合。在中介者模式中,訂閱者是單向的,只能是訂閱者而不能是發佈者。而消息統一由中介者對象發佈。
在不破壞對象的封裝性的前提下,在對象以外捕獲並保存該對象內部狀態以便往後對象使用或者對象恢復到之前的某個狀態。
在前面提到的新聞頁面中,有上一頁和下一頁的按鈕,頁面的內容是用異步請求獲取的。若是點擊下一頁按鈕接着再點擊上一頁那麼以前那一頁又要進行一次異步請求,這是多餘的操做。由於第一次已經獲取了數據,不須要再發送多餘的請求。這個時候能夠用備忘錄模式來緩存請求過的數據。也就是說每次發生請求的時候對當前狀態作一次記錄,將請求到的數據以及對應得頁碼緩存下來,若是以後返回到以前瀏覽過的頁面,直接在緩存中查詢便可,不用發生異步請求。先建立一個新聞緩存器:
// Page備忘錄類 var Page = function() { // 信息緩存對象 var cache = {}; return function(page, fn) { // 判斷該頁數據是否在緩存中 if(cache[page]) { // 顯示該頁內容 showPage(page, cache[page]); // 執行成功回調函數 fn && fn(); } else { // 不然異步請求 $.post('./data/getNewsData.php', { page: page }, function(res) { // 成功返回 if(res.errNo == 0) { showPage(page, res.data); cache[page] = res.data; fn && fn(); } else { // 處理異常 } }) } } }
上面代碼能夠看出Page緩存器內部緩存了每次請求回來的新聞數據,這樣之後若是用戶想回看某頁新聞數據就不須要發送沒必要要的請求了。
備忘錄模式最主要的任務是對現有的數據或狀態進行緩存,爲將類某個時刻使用或恢復作準備。可是當數據量過大時,會嚴重佔用系統提供的資源,此時對緩存器的優化是頗有必要的,複用率低的數據緩存下來是不值得的。
做者把這種模式比喻成一個點鈔機。
在不暴露對象內部結構的同時,能夠順序地訪問聚合對象內部的元素。
迭代器模式主要是解決重複循環迭代的問題,以前接觸過面嚮對象語言的應該都對迭代器有所瞭解。迭代器就是用來順序地訪問一個聚合對象內部元素的,它能夠簡化咱們遍歷操做,就行銀行裏的點鈔機,有了它能夠大幅度下降咱們的點鈔成本。下面建立一個經常使用的迭代器對象:
var Iterator = function(items, container) { // 獲取父元素 var container = container && document.getElementById(container) || document, // 獲取元素 items = container.getElementsByTagName(items), // 獲取元素長度 length = items.length, // 當前索引值 index = 0; // 緩存原生數組splice方法 var splice = [].splice; return { // 獲取第一個元素 first: function() {}, // 獲取最後一個元素 second: function() {}, // 獲取前一個元素 pre: function() {}, // 獲取後一個元素 next: function() {}, // 獲取某一個元素 get: function(num) {}, // 對每個元素執行某一個方法 dealEach: function(fn) {}, // 對某一個元素執行某一個方法 dealItem: function(num, fn) {}, // 排他方式處理某一個元素 exclusive: function() {} } }
下面具體實現迭代器裏面的這些方法,而後就能夠用這個迭代器對象啦。
經過迭代器咱們能夠順序地訪問一個聚合對象中的每個元素。在開發中,迭代器極大簡化了代碼中的循環語句,使代碼結構清晰緊湊。用迭代器去處理一個對象時,只須要提供處理的方法,而沒必要去關心對象的內部結構,這也解決了對象的使用者與對象內部結構之間的耦合。
對於一種語言,給出其文法表示,並定義一種解釋器,經過使用這種解釋器來解釋語言中定義的句子。
一個頁面中的某些功能好壞有時是靠必定的數據依據支撐的。項目經理想看看用戶對最近新增的功能使用狀況,先後端要給出統計數據,然而前端交互統計項中要給出交互元素路徑。這件事情與冒泡事件相似,只不過在這個路徑中還要關心同一層級中當前元素的兄弟元素。好比下面的結構:
<div calss="wrap"> <div class="link-inner"> <a href="#">link</a> </div> <div class="button-inner"> <button>text</button> </div> </div>
要獲取button相對於class爲wrap的div元素的Xpath路徑,那麼能夠表示爲DIV>DIV2>SPAN。
上面對需求的描述是一種文法,描述的是一組規則,如今要作的事實現一個規則解釋器來解釋上面的規則。首先要分析給出的文法,查找他們的類似點,而後該清楚咱們要先實現什麼再實現什麼,基本上問題就能解決了。
一些描述性語句,幾回功能的提取抽象,造成了一套語法法則,這就是解釋器模式要處理的事情。是否能應用解釋器模式的一條重要準則是可否根據需求解析出一套完整的語法規則,不論該語法規則簡單或是複雜都是必須的。
技巧型設計模式是經過一些特定技巧來解決組件的某些方面的問題,這類技巧通常經過實踐經驗總結獲得。這本書中總結了8種技巧型設計模式,分別是鏈模式,委託模式,數據訪問對象模式,節流模式,簡單模板模式,惰性模式,參與者模式和等待者模式。有興趣的同窗能夠去買書來看哦,這裏就不一一解釋了。
架構型設計模式是一類框架結構,經過提供一些子系統,指定它們的職責,並將它們條理清晰地組織在一塊兒。如今流行的前端框架都用了這種類型的設計模式。本書總結了6種架構型設計模式,分別是同步模塊模式,異步模塊模式,Widget模式,MVC模式,MVP模式和MVVM模式。
學習設計模式的學習對於咱們來講任重而道遠,咱們須要在實踐中不斷思考不斷總結。