|
在討論設計模式之前,請確認您已經有一定的腳本編程基礎,如果不甚了解,建議可以先查閱本人很久之前寫的這篇《淺談Javascript面向對象編程》請看下一篇文章。
講到設計模式,不得不先重點著墨于“接口設計”,因為接口設計在設計模式中的意義太大了,大于模式本身。直觀起見,先介紹一下接口定義的形式:
復制代碼 代碼如下:
var interface = new Interface("interface",[["getName",1],["getAge",1]]);
可以看出接口函數必須包含兩個參數,接口方法定義在一個二維數組中。上例中定義了兩個接口方法:getName,getAge,這兩個方法都帶一個參數,下面我們詳細看一下Interface函數的實現代碼,從而加深大家對接口的理解。
復制代碼 代碼如下:
function Interface(name,methods){
if(arguments.length !=2){
console.log("參數必須為二個");
}
this.name = name;
this.methods = [];
if(methods.length<1){
console.log("第二個參數不能為空數組");
}
for(var i=0;len=methods.length,i<len;i++){
if(typeof methods[i][0] !== 'string'){
console.log("第一個參數數據類型必須為字符串");
}
if(methods[i][1] && typeof methods[i][1] !== 'number'){
console.log("第二個參數數據類型必須為整數型");
}
if(methods[i].length == 1){
methods[i][1] = 0;
}
this.methods.push(methods[i]);
}
}
從代碼中不難看出,接口函數的定義規則:[1]Interface函數只能包含兩個參數,第一個參數為接口名稱,第二個參數是一個二維數組[2]第二個參數不允許為空數組[3]methods參數中的第一個參數必須為字符串類型,用以定義方法名,第二個參數必須為整數類型,用以定義方法的參數個數[4]當methods中方法的參數個數為0時,可以省略不寫。
接下來要建一個類,讓該類繼承前面定義的interface接口,那該怎么做呢,別急,我們需要新增一個方法,見如下代碼:
復制代碼 代碼如下:
var ioldfish = function(name,age){
this.name = name;
this.age = age;
Interface.regImplement(this,interface);
}
ioldfish.prototype.getName = function(){
alert(this.name);
};
ioldfish.prototype.getAge = function(){
alert(this.age);
};
var fishwl = new ioldfish("老魚",27);
fishwl.getName();
Interface.regImplement就是我們要新增的方法,作用就是讓ioldfish類按照接口interface的規范編碼,否則將會在firebug的控制臺拋出異常。
看看這個方法的具體實現代碼:
復制代碼 代碼如下:
Interface.regImplement = function(object){
if(arguments.length<2){
console.log("接口繼承參數不能少于二個");
}
for(var i=1;len = arguments.length,i<len;i++){
var interface = arguments[i];
if(interface.constructor !== Interface){
console.log("第三個參數開始必須為接口實例");
}
for(var j=0;len=interface.methods.length,j<len;j++){
var method = interface.methods[j][0];
if(!object[method] || typeof object[method] !=="function" || object[method].getParameters().length !== interface.methods[j][1]){
console.log(""+method+"方法接口不匹配");
}
}
}
}
解讀這段代碼,你很容易發現:[1]Interface.regImplement繼承接口函數的參數至少要有兩個,如果有第三個參數,那么該參數必須是Interface接口的實例[2]我們去遍歷interface接口中的方法,再與新增類中的方法一一匹配,如果發現繼承了該接口規范的類缺少某方法,就會拋出錯誤提示。[3]接口對于參數個數也進行了匹配,如果接口方法中的參數個數與新增類中方法的個數不匹配也會拋出錯誤提示。
為了匹配方法中參數個數,這里用到一個getParameters()方法,我們基于Function做個擴展,代碼實現如下:
復制代碼 代碼如下:
Function.prototype.getParameters = function(){
var str = this.toString();
var paramStr = str.slice(str.indexOf("(")+1,str.indexOf(")")).replace(//s*/g,'');
try{
return (paramStr.length ==0 ? [] : paramStr.split(","));
}
catch(err){
console.log("非法函數");
}
}
接下來,你可以把所講的Interface函數,Interface.regImplement函數,還有Function.prototype.getParameters函數整合到一個interface.js的文件中,調試一下新建的這個ioldfish類。看看當類中缺少getAge方法時會怎么樣?建議新手,各類情況都模擬一下,加強理解吧!如果你確信已經完全理解接口設計,那就跟著我繼續往下走。
Javascript設計模式之單體模式Singleton
單體模式Singleton:這是最基礎的設計模式,嚴格來說沒什么模式可言,但是卻很容易用也很好用,支付寶很多組件都是通過單體模式設計的。事實上在《淺談Javascript面向對象編程》中闡述原型繼承的時候就已經用到了該模式,這里簡單帶過,重點說一下惰性單體,這對一些不是所有用戶都需要,在特定情景下才會用到的組件有非常好的優化作用,他可以讓組件的實例化推遲到用戶觸發他的時候。
復制代碼 代碼如下:
var ioldfish = {
name:'老魚',
age:27,
getName:function(){
alert(name);
},
getAge:function(){
alert(age);
}
}
上例是一個最簡單的單體模式,把本人的資料都整合到ioldfish這個對象字面量中,形成一個模塊,同時起到了一個命名空間的作用。
復制代碼 代碼如下:
var ioldfish =(function(){
var name = '老魚';
var age = 27;
return{
getName:function(){
alert(name);
},
getAge:function(){
alert(age);
}
}
})();
對第一個單體做簡單的修改,通過閉包讓name,age成為靜態私有變量,確保實例化的時候在內存中始終只有一份,這樣更符合單體模式的定義。
下面重點介紹一下惰性單體,廢話少說,先看看我們該如何來實現惰性單體:
復制代碼 代碼如下:
var ioldfish = (function(){
var uniqueInstance;
var name = '老魚';
var age = 27;
function constructor(){
return{
getName:function(){
alert(name);
},
getAge:function(){
alert(age);
}
}
}
return{
isInstance:function(){
if(uniqueInstance == null){
uniqueInstance = constructor();
}
return uniqueInstance;
}
}
})();
ioldfish.isInstance().getName();
上面的結構公私分明一目了然,私有變量uniqueInstance(標識類是否已經實例化)和私有方法constructor,返回一個公有方法isInstance(通過該方法可以調用私有方法constructor中定義的方法),形如:ioldfish.isInstance().getName();先通過isInstance()方法判斷其是否被實例化,然后通過getName()方法獲取到閉包內的私有變量name。該模式的應用場景還是很多的,是不是遇見過頁面中需要加載很大的一個日歷控件,但并非所有用戶都用的到呢?是不是…
Javascript設計模式之工廠模式Factory
工廠模式Factory:先創建一個抽象類,然后基于這個抽象類派生出子類,并在子類中創建工廠方法,從而把實例化推遲到對應的子類中進行,說實話,工廠模式在Javascript中的應用有些牽強,畢竟Javascript不像Java存在硬編碼帶來的困攪,要學習的只是模式的思想,切忌因為模式而模式。
不妨舉個偏激點的例子,為tab切換、下拉列表等組件添加定位,漸隱,延遲等效果,我們可以先為這些組件定義一個接口:
var Iwidget = new Interface("iwidget",[["addEffect"]]);
定義該接口,以便之后派生的子類繼承,接口中定義了一個addEffect方法,接口方法實現后,調用的同學大可不必關注各子類中對于addEffect方法的代碼實現。
復制代碼 代碼如下:
var Widget = function(){};
Widget.prototype={
fire:function(model){
var widget = this.createWidget(model);
//有同學問為什么子類都必須定義接口方法,因為下面要調用嘛
widget.addEffect();
return widget;
},
show:function(){
//show代碼具體實現
},
hide:function(){
//hide代碼具體實現
},
createWidget:function(model){
alert('抽象類,不可以實例化')
}
};
上例先定義一個抽象類Widget,做為派生子類的父類,由于考慮到這兩類組件都涉及到隱藏和顯示一個容器,所以在父類中預先定義好show和hide方法以便子類繼承。
復制代碼 代碼如下:
var xTab = function(){};
extend(xTab,Widget);
xTab.prototype.createWidget = function(model){
var widget;
switch(model){
case 'position':
widget = new xTabPosition();
break;
case 'anim':
widget = new xTabAnim();
break;
case 'delay':
default:
widget = new xTabDelay();
}
};
var dropDown = function(){};
extend(dropDown,Widget);
dropDown.prototype.createWidget = function(model){
var widget;
switch(model){
case 'position':
widget = new dropDownPosition();
break;
case 'anim':
widget = new dropDownAnim();
break;
case 'delay':
default:
widget = new dropDownDelay();
}
};
子類xTab和dropDown繼承了父類,并且重寫了createWidget方法,不同的子類根據定位,漸隱,延遲效果分別創建不同的實例,只要創建這些實例的類都實現接口中約定的addEffect方法,至于方法代碼如何實現,千篇一律,愛咋整咋整。
復制代碼 代碼如下:
var xTabPosition = function(){};
xTabPosition.prototype ={
addEffect:function(){
//具體實現代碼
}
};
var dropDownPosition = function(){};
dropDownPosition.prototype ={
addEffect:function(){
//具體實現代碼
}
};
var dropDownInstance = new dropDown();
dropDownInstance.fire('position');
以此類推,如果您需要為氣泡組件添加這些效果,照葫蘆畫瓢就可以了,說到這里你可以清楚的看到,這種設計模式大大降低了類和類之間的耦合度,而且可以根據具體的交互需求,實現不同的輔助動作,但是也無可避免的增加了代碼實現上的復雜性,事實上這種模式并不適合Javascript,畢竟它有別于Java,不會有類名硬編碼的問題,目的是學習他的設計思想,所以以上示例僅供參考,如無大人在旁,小朋友切勿效仿。
對于Javascript愛好者來說,更有價值的應該是工廠模式中講到的的”緩存(memoization)機制”,書上舉了個創建XHR對象的例子來說明該特性,但是效果顯然不夠明顯……
memoization名詞解釋:把函數的每次執行結果都放入一個鍵值對(數組也可以,視情況而定)中,在接下來的執行中,在鍵值對中查找是否已經有相應執行過的值,如果有,直接返回該值,沒有才 真正執行函數體的求值部分。很明顯,找值,尤其是在鍵值對中找值,比執行函數快多了
在遞歸調用的時候,memoization的威力才能更好的顯現。下面是一個經典的斐波納契序列,fib(20) 會把fib這個方法執行21891次,如果是fib(40),這會執行331160281次。
復制代碼 代碼如下:
function fib(n) {
if (n < 2) {
return n;
}
return fib(n - 1) + fib(n - 2);
}
再看看如何使用memoization來實現:
復制代碼 代碼如下:
var iterMemoFib = (function() {
var cache = [1, 1];
var fib = function(n) {
if (n >= cache.length) {
//將一個遞歸轉換成了一個
for (var i = cache.length; i <= n; i++) {
cache[i] = cache[i - 2] + cache[i - 1];
}
}
return cache[n-1];
}
return fib;
})();
將Function的原型擴展memoize 和unmemoize 方法,這樣你可以對任何函數實現memoize和解除memoize,當然,這個方法要慎,對一些不是頻繁執行的函數,沒必要緩存:
復制代碼 代碼如下:
Function.prototype.memoize = function() {
var pad = {};
var self = this;
var obj = arguments.length > 0 ? arguments[i] : null;
var memoizedFn = function() {
// 把參數作為數組保存,作為鍵,把函數執行的結果作為值緩存起來
var args = [];
for (var i = 0; i < arguments.length; i++) {
args[i] = arguments[i];
}
if (!(args in pad)) {
pad[args] = self.apply(obj, arguments);
}
return pad[args];
}
memoizedFn.unmemoize = function() {
return self;
}
return memoizedFn;
}
Function.prototype.unmemoize = function() {
alert("Attempt to unmemoize an unmemoized function.");
return null;
}
使用方法:fib.memoize();
Javascript設計模式之組合模式
組合模式:運用該設計模式可以通過組合對象添加屬性和方法,通過遞歸批量式的讓葉子對象得到組合對象的屬性和方法。打個比方我們現在要動態創建一個銀行列表,按銀行類型分為網上銀行類,卡通銀行類,并可配置他們是否顯示。用組合模式如何實現呢?
第一步還是先定義接口,因為要做到某類銀行甚至某個銀行是否顯示可配置,那么我們先約定2個接口,showBank和hideBank。
var IcardItem = new Interface(”icardItem”,[["showBank"],["hideBank"]]);
接下來先定義卡的組合對象,并設置組合對象的基本方法add,remove,getChild,由于這個類繼承了IcardItem接口類,所以還定義了showBank,hideBank這兩個接口方法。
復制代碼 代碼如下:
var cardMain = function(id){
this.cards = [];
this.element = document.createElement("div");
this.element.id = id;
Interface.regImplement(this,IcardItem);
};
cardMain.prototype = {
add:function(card){
this.cards.push(card);
this.element.appendChild(card.getElement());
},
remove:function(card){
for(i=0;len=this.cards.length,i<len;i++){
if(cards[i] == card){
this.cards.splice(i,1);
break;
}
this.element.removeChild(card.getElement());
}
},
getChild:function(i){
return this.cards[i];
},
getElement:function(){
return this.element;
},
showBank:function(){
this.element.style.display ="block";
for(i=0;len=this.cards.length,i<len;i++){
this.cards[i].showBank();
}
},
hideBank:function(){
this.element.style.display ="none";
for(i=0;len=this.cards.length,i<len;i++){
this.cards[i].hideBank();
}
}
};
然后定義葉子對象類bankLogo用以創建銀行logo,這里銀行logo都以帶class的a標簽標識:
復制代碼 代碼如下:
var bankLogo = function(bankClassName){
this.element = document.createElement("a");
this.element.className = bankClassName;
Interface.regImplement(this,IcardItem);
};
bankLogo.prototype ={
showBank:function(){
this.element.style.display ="block";
},
hideBank:function(){
this.element.style.display ="none";
},
getElement:function(){
return this.element;
}
};
最后設置一個單體對象,將操作銀行的相關信息形成一個模塊,方便調用:
復制代碼 代碼如下:
var BankAction ={
bankList:[],
addBank:function(card){
this.bankList.push(card);
},
innerBank:function(conId){
for(i=0;len=this.bankList.length,i<len;i++){
var cardObj =this.bankList[i].getElement();
}
document.getElementById(conId).appendChild(cardObj);
}
};
到了實現環節了,實例化生成一個包含所有卡的最外層容器,然后根據卡類,分別生成一個放置銀行卡和卡通卡的容器,最后生成各銀行卡的實例,并按層級關系形成DOM結構:
復制代碼 代碼如下:
var bankDivT = new cardMain("PayCard");//創建最外層容器
var ebankCard = new cardMain("ebankCard");//創建網銀類銀行卡容器
var ktCard = new cardMain("ktCard");//創建卡通類銀行卡容器
var ccbBank = new bankLogo('Ebank-CMB');//創建招行銀行卡
var abcBank = new bankLogo('Ebank-ABC');//創建農行銀行卡
var abcKtBank = new bankLogo('Kt-ABC');//創建卡通農行卡
ebankCard.add(ccbBank);
ebankCard.add(abcBank);
ktCard.add(abcKtBank);
bankDivT.add(ebankCard);
bankDivT.add(ktCard);
BankAction.addBank(bankDivT);
BankAction.innerBank("bankList");
將動態生成的銀行列表,DOM結構形如:
復制代碼 代碼如下:
<div id="PayCard">
<div id="ebankCard">
<a class="Ebank-CMB"></a>
<a class="Ebank-ABC"></a>
</div>
<div id="ktCard">
<a class="Kt-ABC"></a>
</div>
</div>
組合模式應用在動態生成用戶界面的時候,是非常不錯的選擇,他可以很大程度的簡化粘合性代碼,提升維護性。不過還是要慎用,畢竟當葉子對象很多的時候,遞歸還是存在性能上的問題。
Javascript設計模式之裝飾者模式
裝飾者模式:可以不創建新的子類,給對象創建新的功能。舉例說明:支付寶收銀臺紅包結合余額付款的應用場景。
var Ieconomics = new Interface("ieconomics",[["getPrice"]]);
首先創建一個組件類,基與該組件實例化的對象,會被作為參數傳遞給裝飾者類,以便裝飾者調用到該組件中的各方法。
復制代碼 代碼如下:
var economic = function(){
Interface.regImplement(this,Ieconomics);
};
economic.prototype={
getPrice:function(){
//代碼實現
}
};
然后創建一個裝飾者抽象類,作為派生裝飾者選件類的父類:
復制代碼 代碼如下:
var economicsDecorator = function(economic){
this.economic = economic;
this.regImplement(economic,Ieconomics);
};
economicsDecorator.prototype={
getPrice:function(){
return this.economic.getPrice();
}
};
最后基于上面這個抽象類,派生出一個裝飾者選件類:
復制代碼 代碼如下:
//紅包裝飾者選件類
var coupon = function(economic){
//調用裝飾著抽象類的構造函數
economicsDecorator.call(this,economic);
};
extend(coupon,couponDecorator);
coupon.prototype=function(){
//改寫getPrice方法
getPrice:function(){
return this.economic.getPrice() - this.getCoupon();
},
getCoupon:function(){
//獲取紅包總價具體實現
}
};
var myCoupon = new economic();
myCoupon = new coupon(myCoupon);
實現裝飾者模式就是這么簡單,首先創建一個組件的實例myCoupon,然后將該對象作為參數傳遞給裝飾者選件類coupon。你會發現兩句代碼中我都把值賦給了變量myCoupon,這是因為他們都實現了同一個接口類,他們之間是可以互換使用的。
看到這里心細的同學可能會發現,我們在coupon類中新增了一個getCoupon方法,目前來看不會有任何問題,但是如果我們繼續創建一個購物卷裝飾者選件類,然后結合紅包一起用呢?
復制代碼 代碼如下:
//購物卷裝飾者選件類
var voucher = function(economic){
economicsDecorator.call(this,economic);
};
extend(voucher,couponDecorator);
voucher.prototype=function(){
getPrice:function(){
return this.getPrice() - this.getVoucher();
},
getVoucher:function(){
//獲取優惠卷總價具體實現
}
};
var myCoupon = new economic();
myCoupon = new coupon(myCoupon);
myCoupon = new voucher(myCoupon);
在這種場景下面getCoupon方法已經找不到了,這是因為voucher裝飾myCoupon的時候,它的父類economicsDecorator不包含getCoupon方法,那自然是取不到的了,那該怎么辦呢?
分析一下裝飾者抽象類economicsDecorator,我們傳遞了一個myCoupon的引用作為參數,我們可以通過這個參數做一些小動作,獲得新增加的方法。
復制代碼 代碼如下:
var economicsDecorator = function(economic){
this.economic = economic;
this.interface = Ieconomics;
for(var k in this.economic){
if(typeof this.economic[key] !== "function"){
continue;
var i;
for(i = 0;len = this.interface.methods.length,i < len; i++) {
//通過遍歷比較在接口類中是否包含此方法,如果包含返回下一個
if(key == this.interface.methods[i][0]) {
break;
}
}
if(i < this.interface.methods.length)
continue;
var decorator = this;
//采用匿名函數調用方式來定義新方法
(function(methodName) {
decorator[methodName] = function() {
return decorator.economic[methodName]();
};
})(key);
}
}
}
this.regImplement(economic,Ieconomics);
};
economicsDecorator.prototype={
getPrice:function(){
return this.economic.getPrice();
}
};
看上面的代碼,我們對裝飾者抽象類做了一些修改,這樣做是為了確保在裝飾者選件類中一旦定義新方法,可以在裝飾者抽象類中動態的定義出來。這里只是提供一個使用裝飾者模式的思路,具體的實現代碼遠比這個復雜,由于項目還在開發中,demo暫不提供,支付寶新版收銀臺發布后,會跟大家再做個詳細的設計分享。
Javascript設計模式之橋接模式
橋接模式:將抽象和其實現分離開來,以便二者獨立變化。其實很簡單,只是在API和具體事件之間增加一個橋梁,從而降低API和使用他的類和對象之間的耦合。
事實上對大多數同學來說橋接模式并不陌生,下面的this.getName就是一種橋接方法,他是外訪問的一個接口,他的內部實現是通過訪問內部私有變量來實現的,這個方法起到了外部和內部溝通的橋梁作用。
復制代碼 代碼如下:
var ioldfish = function(){
var name = '老魚';
this.getName = function(){
alert(name);
}
}
橋接模式用的最多的還是在事件監聽器回調函數。下面這個是獲取用戶信息的API接口函數:
復制代碼 代碼如下:
function getUserInfo(userid,callback){
asyncRequest('GET','userInfo?userid='+userid,function(resp){
callback(resp.responseText);
});
}
接下去我們要做的是把這個API和某個事件的觸發建立一個橋梁關系
addEvent(element,'click',bridgeMethod);
function bridgeMethod(e){
getUserInfo(this.userid,function(){
//回調函數實現代碼
});
}
這里在element對象click的時候觸發函數并不是getIserInfo,而是新建了一個橋接方法bridgeMethod,通過這層橋接使得API接口函數和click事件相對獨立,這樣大大拓寬了API的適用范圍。
Javascript設計模式之適配器模式
適配器模式:打個比方,你維護了一個系統,之前一直都是用prototype框架,但是現在打算新引入YUI框架,那如何讓兩個框架平穩過度呢
,舉個例子,如何將prototype中的$方法轉換為YUI中的get方法:
復制代碼 代碼如下:
function $(){};
function YAHOO.util.Dom.get=function(el){};
function prototypeToYuiAdapter(){
return YAHOO.util.Dom.get(arguments);
}
你要在prototype中使用yui的get方法,只需要做以下申明即可:
$ = prototypeToYuiAdapter;
這樣的話,在prototype中就可以使用YUI中的get方法了。本人并不是很推崇這種模式,所以不多做闡述,事實上我覺得不到萬不得以,我們根本不需要使用這種模式,作為一名負責任的設計者,我寧可做代碼重構也不希望使用該模式,只能作為無奈之下的過渡型方案使用。
Javascript設計模式之門面模式,觀察者模式
門面模式:這應該是所有腳本框架中都用到的,最基礎的設計模式,隨便找個框架中定義好的方法看看就行了,比如說YUI中的setStyle方法等等等等。在這里就不多闡述了。
觀察者模式:該設計模式應用在Javascript上似乎更為牽強,不甚理解,這里就不說了,以免誤人子第,如有心得者不吝賜教。
一天時間都耗在這篇博文上了,好象還有很多想寫的,看來要把心里想的東西寫清楚還是不容易的,再整理整理再說吧,敬請期待!
JavaScript技術:小議javascript 設計模式 推薦,轉載需保留來源!
鄭重聲明:本文版權歸原作者所有,轉載文章僅為傳播更多信息之目的,如作者信息標記有誤,請第一時間聯系我們修改或刪除,多謝。