閱讀目錄
什麼是設計模式
單體模式:
工廠模式:
單例模式
觀察者模式(發布訂閱模式)
策略模式
模板模式
代理模式
外觀模式
設計模式太多了,貌似有23種,其實我們在平時的工作中沒有必要特意去用什麼樣的設計模式,或者你在不經意間就已經用了設計模式當中的一種。本文旨在總結平時相對來說用的比較多的設計模式。
什麼是設計模式
設計模式(Design pattern)是一套被反復使用、多數人知曉的、經過分類編目的、代碼設計經驗的總結。
使用設計模式是為了可重用代碼、讓代碼更容易被他人理解、保證代碼可靠性。 毫無疑問,設計模式於己於他人於系統都是多贏的;設計模式使代碼編制真正工程化;設計模式是軟件工程的基石脈絡,如同大廈的結構一樣。
實際情況:
設計模式絕對不是紙上談兵的知識,光看書就以為自己懂了,那只是井底之蛙之見,設計模式絕對是從實踐中來到實踐中去的!如果編碼經驗很少,也不太可能能理解好設計模式,但凡軟件設計能力強的人編碼功底都是相當扎實的。
如果沒有能深刻理解面向對象,也不太可能理解好設計模式,剛剛畢業或者才工作一兩年就說自己面向對象能力強的人,基本上就是誇誇其談的人。
很明顯,我就是屬於那種誇誇其談的人,哈哈,不過希望對本文的總結,讓自己更加了解這些設計模式,理解的更加透徹。
單體模式:
概念:
單體是一個用來劃分命名空間並將一批相關的屬性和方法組織在一起的對象,如果他可以被實例化,那麼他只能被實例化一次。
特點:
1.可以來劃分命名空間,從而清除全局變量所帶來的危險。
2.利用分支技術來來封裝浏覽器之間的差異。
3.可以把代碼組織的更為一體,便於閱讀和維護。
代碼實現:
/*Basic Singleton*/
var Singleton = {
attribute:true,
method1:function(){},
method2:function(){}
};
應用場景:
單體模式在我們平時的應用中用的比較多的,相當於把我們的代碼封裝在一個起來,只是暴露一個入口,從而避免全部變量的污染。
工廠模式:
概念:
工廠模式的定義:提供創建對象的接口,意思就是根據領導(調用者)的指示(參數),生產相應的產品(對象)。
創建一個對象常常需要復雜的過程,所以不適合在一個復雜的對象中。
創建對象可能會導致大量的重復代碼,也可能提供不了足夠級別的抽象。
工廠就是把成員對象的創建工作轉交給一個外部對象,好處在於消除對象之間的耦合(也就是相互影響)
分類:
簡單工廠模式:使用一個類,通常為單體,來生成實例。
復雜工廠模式定義是:將其成員對象的實列化推到子類中,子類可以重寫父類接口方法以便創建的時候指定自己的對象類型。
父類只對創建過程中的一般性問題進行處理,這些處理會被子類繼承,子類之間是相互獨立的,具體的業務邏輯會放在子類中進行編寫。
代碼實現:
簡單工廠模式:
var XMLHttpFactory =function(){}; //這是一個簡單工廠模式
XMLHttpFactory.createXMLHttp =function(){
var XMLHttp = null;
if (window.XMLHttpRequest){
XMLHttp = new XMLHttpRequest()
}else if (window.ActiveXObject){
XMLHttp = new ActiveXObject("Microsoft.XMLHTTP")
}
return XMLHttp;
}
//XMLHttpFactory.createXMLHttp()這個方法根據當前環境的具體情況返回一個XHR對象。
var AjaxHander =function(){
var XMLHttp = XMLHttpFactory.createXMLHttp();
...
}
復雜工廠模式:流程==》 先設計一個抽象類,這個類不能被實例化,只能用來派生子類,最後通過對子類的擴展實現工廠方法
var XMLHttpFactory =function(){}; //這是一個抽象工廠模式
XMLHttpFactory.prototype = {
//如果真的要調用這個方法會拋出一個錯誤,它不能被實例化,只能用來派生子類
createFactory:function(){
throw new Error('This is an abstract class');
}
}
var XHRHandler =function(){}; //定義一個子類
// 子類繼承父類原型方法
extend( XHRHandler , XMLHttpFactory );
XHRHandler.prototype =new XMLHttpFactory(); //把超類原型引用傳遞給子類,實現繼承
XHRHandler.prototype.constructor = XHRHandler; //重置子類原型的構造器為子類自身
//重新定義createFactory 方法
XHRHandler.prototype.createFactory =function(){
var XMLHttp =null;
if (window.XMLHttpRequest){
XMLHttp =new XMLHttpRequest();
}else if (window.ActiveXObject){
XMLHttp =new ActiveXObject("Microsoft.XMLHTTP")
}
return XMLHttp;
}
應用場景:
以下幾種情景下工廠模式特別有用:
(1)對象的構建十分復雜
(2)需要依賴具體環境創建不同實例
(3)處理大量具有相同屬性的小對象
優點:
可以實現一些相同的方法,這些相同的方法我們可以放在父類中編寫代碼,那麼需要實現具體的業務邏輯,那麼可以放在子類中重寫該父類的方法,去實現自己的業務邏輯;
也就是說有兩點:
1、弱化對象間的耦合,防止代碼的重復。在一個方法中進行類的實例化,可以消除重復性的代碼。
2、重復性的代碼可以放在父類去編寫,子類繼承於父類的所有成員屬性和方法,子類只專注於實現自己的業務邏輯。
缺點:
當工廠增加到一定程度的時候,提升了代碼的復雜度,可讀性下降。而且沒有解決對象的識別問題,即怎麼知道一個對象的類型。
單例模式
概念:
單例模式定義了一個對象的創建過程,此對象只有一個單獨的實例,並提供一個訪問它的全局訪問點。也可以說單例就是保證一個類只有一個實例,實現的方法一般是先判斷實例存在與否,如果存在直接返回,如果不存在就創建了再返回,這就確保了一個類只有一個實例對象。
代碼實現:
單例的實現有很多種,下面只介紹其中的一種,使用閉包方式來實現單例,代碼如下:
var single = (function(){
var unique;
function getInstance(){
// 如果該實例存在,則直接返回,否則就對其實例化
if( unique === undefined ){
unique = new Construct();
}
return unique;
}
function Construct(){
// ... 生成單例的構造函數的代碼
}
return {
getInstance : getInstance
}
})();
上面的代碼中,unique便是返回對象的引用,而 getInstance便是靜態方法獲得實例。Construct 便是創建實例的構造函數。
可以通過 single.getInstance() 來獲取到單例,並且每次調用均獲取到同一個單例。這就是 單例模式 所實現的效果。
使用場景:
單例模式是一種常用的模式,有一些對象我們往往只需要一個,比如全局緩存、浏覽器的window對象。在js開發中,單例模式的用途同樣非常廣泛。試想一下,當我們
單擊登錄按鈕的時候,頁面中會出現一個登錄框,而這個浮窗是唯一的,無論單擊多少次登錄按鈕,這個浮窗只會被創建一次。因此這個登錄浮窗就適合用單例模式。
總結一下它的使用場景:
1、可以用它來劃分命名空間
2、借助單例模式,可以把代碼組織的更為一致,方便閱讀與維護
觀察者模式(發布訂閱模式)
概念:
定義對象間的一種一對多的依賴關系,以便當一個對象的狀態發生改變時,所有依賴於它的對象都得到通知並自動刷新,也被稱為是發布訂閱模式。
它需要一種高級的抽象策略,以便訂閱者能夠彼此獨立地發生改變,而發行方能夠接受任何有消費意向的訂閱者。
應用場景:
這個模式要先說應用場景,比較好理解。
打一個離我們比較近的一個場景,博客園裡面有一個訂閱的按鈕(貌似有bug),比如小A,小B,小C都訂閱了我的博客,當我的博客一有更新時,就會統一發布郵件給他們這三個人,就會通知這些訂閱者
發布訂閱模式的流程如下:
1. 確定誰是發布者(比如我的博客)。
2. 然後給發布者添加一個緩存列表,用於存放回調函數來通知訂閱者。
3. 發布消息,發布者需要遍歷這個緩存列表,依次觸發裡面存放的訂閱者回調函數。
4、退訂(比如不想再接收到這些訂閱的信息了,就可以取消掉)
代碼如下:
var pubsub = {}; // 定義發布者
(function (q) {
var list = [], //回調函數存放的數組,也就是記錄有多少人訂閱了我們東西
subUid = -1;
// 發布消息,遍歷訂閱者
q.publish = function (type, content) {
// type 為文章類型,content為文章內容
// 如果沒有人訂閱,直接返回
if (!list[type]) {
return false;
}
setTimeout(function () {
var subscribers = list[type],
len = subscribers ? subscribers.length : 0;
while (len--) {
// 將內容注入到訂閱者那裡
subscribers[len].func(type, content);
}
}, 0);
return true;
};
//訂閱方法,由訂閱者來執行
q.subscribe = function (type, func) {
// 如果之前沒有訂閱過
if (!list[type]) {
list[type] = [];
}
// token相當於訂閱者的id,這樣的話如果退訂,我們就可以針對它來知道是誰退訂了。
var token = (++subUid).toString();
// 每訂閱一個,就把它存入到我們的數組中去
list[type].push({
token: token,
func: func
});
return token;
};
//退訂方法
q.unsubscribe = function (token) {
for (var m in list) {
if (list[m]) {
for (var i = 0, j = list[m].length; i < j; i++) {
if (list[m][i].token === token) {
list[m].splice(i, 1);
return token;
}
}
}
}
return false;
};
} (pubsub));
//將訂閱賦值給一個變量,以便退訂
var girlA = pubsub.subscribe('js類的文章', function (type, content) {
console.log('girlA訂閱的'+type + ": 內容內容為:" + content);
});
var girlB = pubsub.subscribe('js類的文章', function (type, content) {
console.log('girlB訂閱的'+type + ": 內容內容為:" + content);
});
var girlC = pubsub.subscribe('js類的文章', function (type, content) {
console.log('girlC訂閱的'+type + ": 內容內容為:" + content);
});
//發布通知
pubsub.publish('js類的文章', '關於js的內容');
// 輸出:
// girlC訂閱的js類的文章: 內容內容為:關於js的內容
// test3.html:78 girlB訂閱的js類的文章: 內容內容為:關於js的內容
// test3.html:75 girlA訂閱的js類的文章: 內容內容為:關於js的內容
//girlA退訂了關於js類的文章
setTimeout(function () {
pubsub.unsubscribe(girlA);
}, 0);
//再發布一次,驗證一下是否還能夠輸出信息
pubsub.publish('js類的文章', "關於js的第二篇文章");
// 輸出:
// girlB訂閱的js類的文章: 內容內容為:關於js的第二篇文章
// girlC訂閱的js類的文章: 內容內容為:關於js的第二篇文章
代碼可以自己運行一遍,這樣比較好理解
優缺點:
優點:當我們需要維護相關對象的一致性的時候,使用觀察者模式,,就可以避免對象之間的緊密耦合。例如,一個對象可以通知另外一個對象,而不需要知道這個對象的信息。
缺點:在發布/訂閱模式中,如果我們需要將發布者同訂閱者上解耦,將會在一些情況下,導致很難確保我們應用中的特定部分按照我們預期的那樣正常工作。也就是說它的優點也可能是它的缺點
策略模式
概念:
策略模式指的是定義一些列的算法,把他們一個個封裝起來,目的就是將算法的使用與算法的實現分離開來。說白了就是以前要很多判斷的寫法,現在把判斷裡面的內容抽離開來,變成一個個小的個體。
代碼實現:
代碼情景為超市促銷,vip為5折,老客戶3折,普通顧客沒折,計算最後需要支付的金額。
沒有使用策略模式的情況:
function Price(personType, price) {
//vip 5 折
if (personType == 'vip') {
return price * 0.5;
}
else if (personType == 'old'){ //老客戶 3 折
return price * 0.3;
} else {
return price; //其他都全價
}
}
不足之處:不好的地方,當我有其他方面的折扣時,又或者我活動的折扣時經常變化的,這樣就要不斷的修改if..else裡面的條件了。而且也違背了設計模式的一個原則:對修改關閉,對擴展開放的原則;
使用策略模式之後:
// 對於vip客戶
function vipPrice() {
this.discount = 0.5;
}
vipPrice.prototype.getPrice = function(price) {
return price * this.discount;
}
// 對於老客戶
function oldPrice() {
this.discount = 0.3;
}
oldPrice.prototype.getPrice = function(price) {
return price * this.discount;
}
// 對於普通客戶
function Price() {
this.discount = 1;
}
Price.prototype.getPrice = function(price) {
return price ;
}
// 上下文,對於客戶端的使用
function Context() {
this.name = '';
this.strategy = null;
this.price = 0;
}
Context.prototype.set = function(name, strategy, price) {
this.name = name;
this.strategy = strategy;
this.price = price;
}
Context.prototype.getResult = function() {
console.log(this.name + ' 的結賬價為: ' + this.strategy.getPrice(this.price));
}
var context = new Context();
var vip = new vipPrice();
context.set ('vip客戶', vip, 200);
context.getResult(); // vip客戶 的結賬價為: 100
var old = new oldPrice();
context.set ('老客戶', old, 200);
context.getResult(); // 老客戶 的結賬價為: 60
var Price = new Price();
context.set ('普通客戶', Price, 200);
context.getResult(); // 普通客戶 的結賬價為: 200
通過策略模式,使得客戶的折扣與算法解藕,又使得修改跟擴展能獨立的進行,不影到客戶端或其他算法的使用;
使用場景:
策略模式最實用的場合就是某個“類”中包含有大量的條件性語句,比如if...else 或者 switch。每一個條件分支都會引起該“類”的特定行為以不同的方式作出改變。以其維
護一段龐大的條件性語句,不如將每一個行為劃分為多個獨立的對象。每一個對象被稱為一個策略。設置多個這種策略對象,可以改進我們的代碼質量,也更好的進行單元測試。
模板模式
概念:
定義了一個操作中的算法的骨架,而將一些步驟延遲到子類中。模板方法使得子類可以不改變一個算法的結構即可重定義該算法的某些特定步驟。
通俗的講,就是將一些公共方法封裝到父類,子類可以繼承這個父類,並且可以在子類中重寫父類的方法,從而實現自己的業務邏輯。
代碼實現:
比如前端面試,基本包括筆試,技術面試,領導面試,HR面試等,但是每個公司的筆試題,技術面可能不一樣,也可能一樣,一樣的就繼承父類的方法,不一樣的就重寫父類的方法
var Interview = function(){};
// 筆試
Interview.prototype.writtenTest = function(){
console.log("這裡是前端筆試題");
};
// 技術面試
Interview.prototype.technicalInterview = function(){
console.log("這裡是技術面試");
};
// 領導面試
Interview.prototype.leader = function(){
console.log("領導面試");
};
// 領導面試
Interview.prototype.HR = function(){
console.log("HR面試");
};
// 等通知
Interview.prototype.waitNotice = function(){
console.log("等通知啊,不知道過了沒有哦");
};
// 代碼初始化
Interview.prototype.init = function(){
this.writtenTest();
this.technicalInterview();
this.leader();
this.HR();
this.waitNotice();
};
// 阿裡巴巴的筆試和技術面不同,重寫父類方法,其他繼承父類方法。
var AliInterview = function(){};
AliInterview.prototype = new Interview();
// 子類重寫方法 實現自己的業務邏輯
AliInterview.prototype.writtenTest = function(){
console.log("阿裡的技術題就是難啊");
}
AliInterview.prototype.technicalInterview = function(){
console.log("阿裡的技術面就是叼啊");
}
var AliInterview = new AliInterview();
AliInterview.init();
// 阿裡的技術題就是難啊
// 阿裡的技術面就是叼啊
// 領導面試
// HR面試
// 等通知啊,不知道過了沒有哦
應用場景:
模板模式主要應用在一些代碼剛開要一次性實現不變的部分。但是將來頁面有修改,需要更改業務邏輯的部分或者重新添加新業務的情況。主要是通過子類來改寫父類的情
況,其他不需要改變的部分繼承父類。
代理模式
概念:
代理模式的中文含義就是幫別人做事,javascript的解釋為:把對一個對象的訪問, 交給另一個代理對象來操作.
代碼實現:
比如我們公司的補打卡是最後是要交給大boss來審批的,但是公司那麼多人,每天都那麼多補打卡,那大boss豈不是被這些瑣事累死。所以大boss下會有一個助理,來幫
忙做這個審批,最後再將每個月的補打卡統一交給大boss看看就行。
// 補打卡事件
var fillOut = function (lateDate) {
this.lateDate = lateDate;
};
// 這是bigBoss
var bigBoss = function (fillOut) {
this.state = function (isSuccess) {
console.log("忘記打卡的日期為:" + fillOut.lateDate + ", 補打卡狀態:" + isSuccess);
}
};
// 助理代理大boss 完成補打卡審批
var proxyAssis = function (fillOut) {
this.state = function (isSuccess) {
(new bigBoss(fillOut)).state(isSuccess); // 替bigBoss審批
}
};
// 調用方法:
var proxyAssis = new proxyAssis(new fillOut("2016-9-11"));
proxyAssis.state("補打卡成功");
// 忘記打卡的日期為:2016-9-11, 補打卡狀態:補打卡成功
應用場景:
比如圖片的懶加載,我們就可以運用這種技術。在圖片未加載完成之前,給個loading圖片,加載完成後再替換成實體路徑。
var myImage = (function(){
var imgNode = document.createElement("img");
document.body.appendChild(imgNode);
return function(src){
imgNode.src = src;
}
})();
// 代理模式
var ProxyImage = (function(){
var img = new Image();
img.onload = function(){
myImage(this.src);
};
return function(src) {
// 占位圖片loading
myImage("http://img.lanrentuku.com/img/allimg/1212/5-121204193Q9-50.gif");
img.src = src;
}
})();
// 調用方式
ProxyImage("https://img.alicdn.com/tps/i4/TB1b_neLXXXXXcoXFXXc8PZ9XXX-130-200.png"); // 真實要展示的圖片
當然,這種懶加載方法不用代理模式也是可以實現的,只是用代理模式。我們可以讓 myImage 只做一件事,只負責將實際圖片加入到頁面中,而loading圖片交給ProxyImage去做。從而降低代碼的耦合度。因為當我不想用loading的時候,可以直接調用myImage 方法。也即是說假如我門不需要代理對象的話,直接可以換成本體對象調用該方法即可。
外觀模式
概念:
外觀模式是很常見。其實它就是通過編寫一個單獨的函數,來簡化對一個或多個更大型的,可能更為復雜的函數的訪問。也就是說可以視外觀模式為一種簡化某些內容的手段。
說白了,外觀模式就是一個函數,封裝了復雜的操作。
代碼實現:
比如一個跨浏覽器的ajax調用
function ajaxCall(type,url,callback,data){
// 根據當前浏覽器獲取對ajax連接對象的引用
var xhr=(function(){
try {
// 所有現代浏覽器所使用的標准方法
return new XMLHttpRequest();
}catch(e){}
// 較老版本的internet Explorer兼容
try{
return new ActiveXObject("Msxml2.XMLHTTP.6.0");
}catch(e){}
try{
return new ActiveXObject("Msxml2.XMLHTTP.3.0");
}catch(e){}
try{
return new ActiveXObject("Microsoft.XMLHTTP");
}catch(e){}
// 如果沒能找到相關的ajax連接對象,則跑出一個錯誤。
throw new Error("Ajax not support in this browser.")
}()),
STATE_LOADED=4,
STATUS_OK=200;
// 一但從服務器收到表示成功的相應消息,則執行所給定的回調方法
xhr.onreadystatechange=function{
if(xhr.readyState !==STATE_LOADED){
return;
}
if(xhr.state==STATUS_OK){
callback(xhr.responseText);
}
}
// 使用浏覽器的ajax連接對象���向所給定的URL發出相關的調用
xhr.open(type.toUpperCase(),url);
xhr.send(data);
}
// 使用方法
ajaxCall("get","/user/12345",function(rs){
alert('收到的數據為:'+rs);
})
應用場景:
當需要通過一個單獨的函數或方法來訪問一系列的函數或方法調用,以簡化代碼庫的其余內容,使得代碼更容易跟蹤管理或者更好的維護時,可以使用外觀模式。其實我們平時代碼中這種模式應該是用的比較多的。
JavaScript的設計模式有很多種,本文只是總結了其中的幾種,以後可能會補充。這篇文章下來查閱了挺多資料,也學到挺多東西的。