設計模式有不少種, 但在前端中如何運用?在前端編碼實踐中, 常見的設計模式有哪些?前端
設計模式, 是在面向對象軟件設計中針對特定問題的簡潔&優雅的解決方案. 在不一樣的編程語言中, 對設計模式的實現, 可能會有區別. 好比Java和JavaScript, 在Java這種靜態編譯型語言中, 沒法動態地給已存在的對象添加職責, 因此通常經過包裝類的方式來實現裝飾者模式. 但在JavaScript中, 給對象動態添加職責是很簡單的事, 這也就形成了JavaScript語言的裝飾者模式再也不關注於給對象動態添加職責, 而是關注於給函數動態添加職責.ajax
工廠模式, 是用來建立對象的一種經常使用的設計模式, 不暴露建立對象的具體邏輯, 而是將邏輯封裝在一個函數中. 即這個函數能夠視爲一個工廠, 工廠模式根據抽象程度的不一樣能夠分爲:算法
下面主要來介紹下簡單工廠和工廠就去在JavaScript中的運用的一些簡單示例.編程
簡單工廠,又稱爲靜態工廠, 由一個工廠對象決策建立某一種產品對象類的實例, 主要用來建立同一類對象.設計模式
例如, 在實際項目開發中, 咱們可能須要根據用戶權限來渲染不一樣的頁面, 高級權限的用戶所擁有的頁面有些是沒法被低級權限的用戶所查看. 所以咱們能夠在不一樣權限等級用戶的構造函數中, 保存該用戶可以看到的頁面.瀏覽器
function UserFactory(role){
function SuperAdmin(){
this.name = 'superManager'
this.viewPage = ['home', 'userManage', 'orderManage', 'appManage', 'permManage']
}
function Admin(){
this.name = 'admin'
this.viewPage = ['home', 'orderMange', 'appManage']
}
function NormalUser(){
this.name = 'normalUser'
this.viewPage = ['home', 'orderManage']
}
switch(role){
case 'superAdmin':
return new SuperAdmin()
break
case 'admin':
return new Admin()
break
case 'user':
return new NormalUser()
break
default:
throw new Error('參數錯誤, 可選參數:superAdmin、admin、user')
}
}
let superAdmin = UserFactory('superAdmin')
let admin = UserFactory('admin')
let normalUser = UserFactory('user')
複製代碼
在上面的示例中, UserFactory就是一個簡單工廠, 在該函數中有3個構造函數, 分別對應不一樣權限的用戶, 當咱們調用工廠函數時, 只須要傳遞superAdmin, admin, user這3個可選參數, 便可獲取一個對應的實例對象.緩存
工廠方法, 是將實際建立對象的工做推遲到子類中, 這樣核心類就變成了抽象類, 可是在JavaScript中, 很難像傳統面向對象那樣去建立抽象類, 所以在JavaScript中咱們只要參考它的核心思想便可, 咱們能夠將工廠方法看做是一個實例化對象的工廠類.安全
好比, 上面的例子, 咱們使用工廠方法來改造下. 工廠方法, 咱們只把它看做是一個實例化對象的工廠, 它只作實例化對象這一件事情, 咱們採用安全模式建立對象.服務器
function UserFactory(role){
if(this instanceof UserFactory){
if(typeof this[role] !== 'function') throw new Error('參數錯誤, 可選參數:superAdmin、admin、user')
const s = new this[role]();
return s;
}else {
return new UserFactory(role)
}
}
UserFactory.prototype = {
SuperAdmin: function (){
this.name = 'superManager'
this.viewPage = ['home', 'userManage', 'orderManage', 'appManage', 'permManage']
},
Admin: function (){
this.name = 'admin'
this.viewPage = ['home', 'orderMange', 'appManage']
},
NormalUser: function (){
this.name = 'normalUser'
this.viewPage = ['home', 'orderManage']
}
}
const superAdmin = UserFactory('SuperAdmin');
const admin = UserFactory('Admin')
const normalUser = UserFactory('NormalUser')
const user = UserFactory('user')
複製代碼
在簡單工廠中,若是咱們新增長一個用戶類型,須要修改兩處地方的代碼:markdown
而在抽象工廠方法中,咱們只須要在UserFactory.prototype中添加就能夠啦。
單例模式, 保證一個類只有一個實例, 而且提供一個訪問它的全局訪問點
在一些特定的需求場景中, 咱們須要保證一個對象只需一個, 例如:
實現思路: 用一個變量標識當前是否已經爲某個類建立過對象, 若是是, 則在下一次獲取這個類的實例時, 直接返回以前建立的對象.
這樣作的優勢是:
下面咱們來看一個簡單的示例, 在JavaScript中咱們可使用閉包來實現這種模式:
var cls = (function(){
var instance;
function getInstance(){
if(instance === undefined){
instance = new Construct()
}
return instance;
}
function Construct(){
// ... 構造函數
}
return {
getInstance: getInstance
}
})()
複製代碼
在上面的代碼中,咱們可使用cls.getInstance來獲取到單例,而且每次調用均獲取到同一個單例.
在咱們平時的開發中,咱們也常常會用到這種模式,好比當咱們單擊登陸按鈕的時候,頁面中會出現一個登陸框,而這個浮窗是惟一的,不管單擊多少次登陸按鈕,這個浮窗只會被建立一次,所以這個登陸浮窗就適合用單例模式。
代理模式是一種很是有意義的模式, 在生活中能夠找到不少代理模式的場景.
好比明星都有經紀人做爲代理. 若是想請明星來辦一場 商業演出, 只能聯繫他的經紀人. 經紀人會把商業演出的細節和報酬都談好以後, 再把合同交給明星籤. 代理模式的關鍵是, 當客戶不方便直接訪問一個對象或者不知足須要的時候, 提供一個替身對象來控制對這個對象的訪問, 客戶實際 上訪問的是替身對象. 替身對象對請求作出一些處理以後, 再把請求轉交給本體對象.
在Web開發中, 圖片預加載是一種經常使用的技術, 若是直接給某個img標籤節點設置src屬性, 因爲圖片過大或者網絡不佳, 圖片的位置 每每有段時間會是一片空白. 常見的作法是先用一張loading圖片佔位, 而後用異步的方式加載圖片, 等圖片加載好了再把它填充 到img節點裏, 這種場景就很適合使用虛擬代理.
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){
myImage("file:// /C:/Users/sven/Desktop/loading.gif");
img.src = src;
}
})();
proxyImage("http://img/cache/com/music/dklddsafla.jpg");
複製代碼
在Web開發中, 也許最大的開銷就是網絡請求. 假設咱們在作一個文件同步的功能, 當咱們選中一個checkbox的時候, 它對應的文件 就會被同步到另外一臺備用服務器上.當咱們選中3個checkbox的時候,依次往服務器發送了3次同步文件的請求. 而點擊並非一個很復 雜的操做, 一秒內點中4個checkbox並非什麼難事, 如此頻繁的網絡請求將會帶來至關大的開銷. 解決方案是, 能夠經過一個代理函數proxySynchronousFile來收集一段時間內的請求, 最後一次性發送給服務器.好比咱們等待2秒之 後才把這2秒以內須要同步的文件ID打包發給服務器, 若是不是對實時性要求很是高的系統, 2秒的延遲不會帶來太大的反作用, 卻能 大大減輕服務器的壓力.代碼以下:
var synchronousFile = function(id){
console.log("開始同步文件, id爲: " + id);
};
var proxySynchronousFile = (function(){
var cache = [];
var timer;
return function(id){
cache.push(id);
if(timer){
return;
}
timer = setTimeout(function(){
synchronousFile(cache.join(","));
clearTimeout(timer);
timer = null;
cache.length = 0;
}, 2000);
};
})();
var checkbox = document.getElementsByTagName("input");
for(var i= 0, c; c=checkbox[i++];){
c.onclick = function(){
if(this.checked === true){
proxySynchronousFile(this.id);
}
}
}
複製代碼
策略模式, 指的是定義一些列的算法,把他們一個個封裝起來,目的就是將算法的使用與算法的實現分離開來,避免多重判斷條件,更具備擴展性。
舉個例子,如今超市有活動,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; //其餘都全價
}
}
複製代碼
在上面的代碼中,咱們須要不少個判斷,若是有不少優惠,咱們又須要添加不少判斷,這裏已經違背了剛纔說的設計模式的六大原則中的開閉原則了,若是使用策略模式,咱們的代碼能夠這樣寫:
class Customer{
constructor(name,discount){
this.name = name
this.discount = discount
}
getPrice(price){
return this.discount * price
}
hello(){
throw new Error("方法未實現!")
}
}
class VipCustomer extends Customer{
constructor(username){
super('vip', 0.5)
this.username = username
}
hello(){
return `尊敬的VIP用戶: ${this.username} 你好!`
}
}
class OldCustomer extends Customer{
constructor(username){
super('normal', 0.89)
this.username = username
}
hello(){
return `尊敬的老用戶: ${this.username} 你好!`
}
}
const vip = new VipCustomer('小平')
window.console.log(vip.hello())
window.console.log("vip客戶 的結帳價爲:", vip.getPrice(200))
const old = new OldCustomer('小軍')
window.console.log(old.hello())
window.console.log("普通客戶 的結帳價爲:", old.getPrice(200))
複製代碼
總結:在上面的代碼中,經過策略模式,使得客戶的折扣與算法解藕,又使得修改跟擴展能獨立的進行。
當咱們的代碼中有不少個判斷分支,每個條件分支都會引發該「類」的特定行爲以不一樣的方式做出改變,這個時候就可使用策略模式,能夠改進咱們代碼的質量,也更好的能夠進行單元測試。
發佈-訂閱模式的通用實現
假如咱們正在開發一個商城網站, 網站裏有header頭部, nav導航, 消息列表, 購物車等模塊. 這幾個模塊的渲染有一個共同的前提 條件, 就是必須先用ajax異步請求獲取用戶的登陸信息. 至於ajax請求何時能成功返回用戶信息, 這點沒法肯定. 如今的情節 看起來像極了售樓處的例子, 小明不知道何時開發商的售樓手續可以成功辦下來. 更重要的一點是, 咱們不知道除了header頭部, nav導航, 消息列表, 購物車以外, 未來還有哪些模塊須要使用這些用戶信息. 若是 它們和用戶信息模塊產生了強耦合, 好比下面這樣的形式:
login.succ(function(data){
header.setAvatar(data.avatar); // 設置header模塊的頭像
nav.setAvatar(data.avatar); // 設置導航模塊的頭像
message.refresh(); // 刷新消息列表
cart.refresh(); // 刷新購物車列表
});
複製代碼
這種耦合性會使程序變得僵硬, header模塊不能隨意再改變setAvatar的方法名, 它自身的名字也不能被改成header1, header2.這是 針對實現編程的典型例子, 針對具體實現編程是不被贊同的. 用發佈-訂閱模式重寫以後, 對用戶信息感興趣的業務模塊將自行訂閱登陸成功的消息事件. 當登陸成功時, 登陸模塊只須要發佈登陸 成功的消息, 而業務方接受到消息以後, 就會開始進行各自的業務處理, 登陸模塊並不關心業務方究竟要作什麼, 也不想去了解它們 的內部細節. 改善後的代碼下:
$.ajax("http://xxx.com?login", function(data){ // 登陸成功
login.trigger("loginSucc", data); // 發佈登陸成功的消息
});
// 和模塊監聽登陸成功的消息
var header = (function(){ // header模塊
login.listen("loginSucc", function(data){
header.setAvatar(data.avatar);
});
return {
setAvatar: function(data){
console.log("設置header模塊的頭像!");
}
}
})();
var nav = (function(){ // nav模塊
login.listen("loginSucc", function(data){
nav.setAvatar(data.avatar);
});
return {
setAvatar: function(avatar){
console.log("設置nav模塊的頭像!");
}
}
})();
var address = (function(){ // 收貨地址模塊
login.listen("loginSucc", function(obj){
address.refresh(obj);
});
return {
refresh: function(avatar){
console.log("刷新收貨地址列表!");
}
}
})();
複製代碼
在程序中, 發佈-訂閱模式能夠用一個全局的Event對象來實現, 訂閱者不須要了解消息來自哪一個發佈者, 發佈者也不知道消息會推送 給哪些訂閱者, Event做爲一個相似"中介者"的角色, 把訂閱者和發佈者聯繫起來.見以下代碼:
但這裏咱們要留意另外一個問題, 模塊之間若是用了太多的全局發佈-訂閱模式來通訊, 那麼模塊與模塊之間的聯繫就被隱藏到 背後. 咱們最終會搞不清楚消息來自哪一個模塊, 或者消息會流向哪些模塊, 這又會給咱們的維護帶來一些麻煩, 也許某個模塊 的做用就是暴露一些接口給其餘模塊調用.
咱們所瞭解的發佈-訂閱模式中, 都是訂閱者先訂閱一個消息, 隨後才能接收到發佈者發佈的消息. 若是把順序返過來, 發佈者先發 布一條消息, 而在此以前並無對象來訂閱它, 這條消息無疑將消失在宇宙中. 在某些狀況下, 咱們須要先將這條消息保存下來, 等到有對象來訂閱它的時候, 再從新把消息發佈給訂閱者. 就如同QQ中的離線消息 同樣, 離線消息被保存在服務器中, 接收人下次登陸上線以後, 能夠從新收到這條消息. 這種需救濟在實際項目中是存在的, 好比在以前折商城網站中, 獲取到用戶信息以後才能渲染用戶導航模塊, 而獲取用戶信息的操做 是一個ajax異步請求. 當ajax請求成功返回以後會發佈一個事件, 在此以前訂閱了此事件的用戶導航模塊能夠接收到這些用戶信息. 但這只是理想的情況, 由於異步的緣由, 咱們不能保證ajax請求返回的時間, 有時它返回得比較快, 而此時用戶導航模塊的代碼尚未 加載好(尚未訂閱相應的事件), 特別是在用了一些模塊化惰性加載的技術後, 這是極可能發生的事情. 也許咱們還須要一個 方案, 使得咱們的發佈-訂閱對象擁有先發布後訂閱的能力. 爲了知足這個需求, 咱們要創建一個存放離線事件的堆棧, 當事件發佈的時候, 若是此時尚未訂閱者來訂閱這個事件, 咱們暫時把 發佈事件的動做包裹在一個函數裏, 這些包裝函數將被存入堆棧中, 等到終於有對象來訂閱此事件的時候, 咱們將遍歷堆棧而且依次 執行這些包裝函數, 也就是從新發布里面的事件.固然離線事件的生命週期只有一次, 就像QQ的未讀信息只會被從新閱讀一次, 因此 剛纔的操做咱們只能進行一次.