javascript設計模式與應用
目錄
前言
設計模式真的很多很複雜,建議有興趣的看我文章最後的連結去學習,要想掌握和熟練應用到專案中絕對不是一蹴而就的,我這篇文章頂多就是一個入門級別的學習,讓大家對設計模式有個概念,我講的也非常簡單,程式碼太長我自己都懶得看,所以儘量舉簡單的例子,說實話,設計模式我啃得也很痛苦,說多了都是淚,哈哈!最後說明一下,文章並未列出23種設計模式,我只按照我覺得重要和使用多的講了上面11個,也並非所有的模式都寫了實現程式碼,後續有時間和必要的話,可能還會更新其他的模式。
設計模式
建構函式模式
用建構函式來生成物件
// 例項共享的方法定義在原型上,例項本身的屬性定義在建構函式裡面
function Car( model, year, miles ) {
this.model = model;
this.year = year;
this.miles = miles;
}
// 這裡注意要在原型上新增方法,而不是給原型賦值,不然就會丟失原型
Car.prototype.toString = function () {
return this.model + " has done " + this.miles + " miles";
};
// Usage:
var civic = new Car( "Honda Civic", 2009, 20000 );
var mondeo = new Car( "Ford Mondeo", 2010, 5000 );
console.log( civic.toString() );
console.log( mondeo.toString() );
複製程式碼
工廠模式(Factory)
- 定義: 工廠模式是用來建立物件的一種最常用的設計模式。我們不暴露建立物件的具體邏輯,而是將將邏輯封裝在一個函式中,那麼這個函式就可以被視為一個工廠。工廠模式根據抽象程度的不同可以分為:簡單工廠,工廠方法和抽象工廠。除非專案很複雜,否則一般用不到工廠方法和抽象方法
- 應用:jQuery的$選擇器就是用工廠模式建立的
// 簡單模擬一下jQuery的實現
class jQuery {
constructor(selector) {
let slice = Array.prototype.slice
let dom = slice.call(document.querySelectorAll(selector))
let len = dom.length? dom.length: 0
for (let i = 0; i < len; i++) {
this[i] = dom[i]
}
this.length = len
this.selector = selector
}
addClass() {
}
...
}
// $就是一個工廠
window.$ = function(selector) {
return new jQuery(selector)
}
複製程式碼
單例模式(Singleton)
- 系統中只能有一個例項,一個類只能建立一個例項, 比如登陸框,購物車, vuex和redux的store也是單例
- 來實現一個單例
class Singleton {
login() {
}
}
Singleton.getInstance = (function() {
let instance
return function() {
if(!instance) {
instance = new Singleton()
}
return instance
}
})()
let obj1 = Singleton.getInstance()
let obj2 = Singleton.getInstance()
console.log(obj1 === obj2) // true
複製程式碼
代理模式(Proxy)
- 使用者無權訪問目標物件, 為其他物件提供一種代理以控制對這個物件的訪問, 介面不變
- 網頁事件代理,jQuery.$.proxy, es6的Proxy
- es6的代理proxy
- 在 Vue3.0 中將會通過 Proxy 來替換原本的 Object.defineProperty 來實現資料響應式, 接下來我們自己用Proxy實現一下吧
let onWatch = (obj, setBind, getLogger) => {
let handler = {
get(target, property, receiver) {
getLogger(target, property);
return Reflect.get(target, property, receiver);
},
set(target, property, value, receiver) {
setBind(value, property);
return Reflect.set(target, property, value);
}
};
return new Proxy(obj, handler);
};
let obj = { a: 1 };
let p = onWatch(
obj,
(v, property) => {
console.log(`監聽到屬性${property}改變為${v}`);
},
(target, property) => {
console.log(`'${property}' = ${target[property]}`);
}
);
p.a = 2; // 監聽到屬性a改變
p.a; // 'a' = 2
複製程式碼
觀察者模式(Observer)
- 主題和觀察者分離,當主題更新的時候,通知所有的觀察者更新自己
- 應用:vue的響應式,node的eventEmitter, 我們來實現一個簡單的EventEmitter
- 觀察者模式和釋出訂閱者模式還是有點區別的,在這裡不做區分了
class EventEmitter {
constructor() {
this._events = {}; // 維護訂閱者列表
}
// 訂閱主題
on(name, fn) {
if (name in this._events) {
// 避免重複訂閱
if(!this._events[name].find(f => f === fn)) {
this_events[name].push(fn);
}
} else {
this._events[name] = [];
this._events[name].push(fn);
}
}
// 釋出主題,相關主題的訂閱者更新
emit(name, ...arg) {
if (name in this._events) {
let events = this._events[name];
for (let i = 0; i < events.length; i++) {
events[i](...arg);
}
}
}
// 取消訂閱
off(name, fn) {
if (name in this._events) {
let index = this._events[name].findIndex(f => f === fn);
if (index > -1) {
this._events[name].splice(index, 1);
}
}
}
}
let event = new EventEmitter();
function subFn(data) {
console.log(data);
}
// 訂閱主題
event.on("vue", subFn);
// 釋出通知
event.emit("vue", "vue3.0要出來了"); // "vue3.0要出來了"
// 取消訂閱
event.off("vue", subFn);
// 再發布通知,就不會列印了
event.emit("vue", "vue3.0馬上要出來了");
複製程式碼
介面卡模式(Adaptor)
- 介面卡模式(Adapter)是將一個類(物件)的介面(方法或屬性)轉化成客戶希望的另外一個介面(方法或屬性),介面卡模式使得原本由於介面不相容而不能一起工作的那些類(物件)可以一些工作。
- 舊介面和使用者分離
裝飾器模式(Decorator)
- 為物件新增新功能,不改變原有的結構和功能, 優點是把類(函式)的核心職責和裝飾功能區分開了
- ES7已經有了裝飾器
阮一峰的ES6教程中對裝飾器講的很好很全面,大家可以去看看,連結在此(es6.ruanyifeng.com/#docs/decor…)
迭代器模式(Iterator)
- 提供一種方法順序一個聚合物件中各個元素,而又不暴露該物件內部表示。
- 迭代器的幾個特點是:
- 訪問一個聚合物件的內容而無需暴露它的內部表示。
- 為遍歷不同的集合結構提供一個統一的介面,從而支援同樣的演算法在不同的集合結構上進行操作。
- 遍歷的同時更改迭代器所在的集合結構可能會導致問題(比如C#的foreach裡不允許修改item。
- 應用
- es6的Iterator es6的有序資料集合(Array, string, Map, Set, generator等)都部署了[Symbol.iterator]屬性, 這個屬性是一個方法,返回一個迭代器,因此都可以用for...of遍歷
- jQuery裡一個非常有名的迭代器就是$.each方法,通過each我們可以傳入額外的function,然後來對所有的item項進行迭代操作,例如:
$.each(['dudu', 'dudu', '酸奶小妹', '那個MM'], function (index, value) {
console.log(index + ': ' + value);
});
//或者
$('li').each(function (index) {
console.log(index + ': ' + $(this).text());
});
複製程式碼
外觀模式(Facade)
- 定義
- 為子系統中的一組介面提供了一個一致的介面,此模組定義了一個高層介面,這個介面使得這一子系統更加容易使用。
- 特點
-
外觀模式不僅簡化類中的介面,而且對介面與呼叫者也進行了解耦。外觀模式經常被認為開發者必備,它可以將一些複雜操作封裝起來,並建立一個簡單的介面用於呼叫。
-
外觀模式經常被用於JavaScript類庫裡,通過它封裝一些介面用於相容多瀏覽器,外觀模式可以讓我們間接呼叫子系統,從而避免因直接訪問子系統而產生不必要的錯誤。
-
外觀模式的優勢是易於使用,而且本身也比較輕量級。但也有缺點 外觀模式被開發者連續使用時會產生一定的效能問題,因為在每次呼叫時都要檢測功能的可用性
-
- js中ie瀏覽器的事件api和其他瀏覽器的不同,為了相容,我們一般都會封裝一個統一的事件處理函式
var addMyEvent = function (el, event, fn) {
if (el.addEventListener) {
el.addEventListener(event, fn, false);
} else if (el.attachEvent) {
el.attachEvent('on' + event, fn);
} else {
el['on' + event] = fn;
}
};
複製程式碼
狀態模式(State)
- 一個物件有狀態變化,每個狀態下有不同的行為
- 場景:
- 有限狀態機
- es6的Promise
- 實現一個Promise, 甩連結,有興趣的同學可以去研究
大白話promise
談談 ES6 的 Promise 物件
手把手教你實現一個完整的 Promise
promise原始碼分析
【翻譯】Promises/A+規範
命令模式(Command)
- 命令模式(Command)的定義是:用於將一個請求封裝成一個物件,從而使你可用不同的請求對客戶進行引數化;對請求排隊或者記錄請求日誌,以及執行可撤銷的操作。也就是說改模式旨在將函式的呼叫、請求和操作封裝成一個單一的物件,然後對這個物件進行一系列的處理。此外,可以通過呼叫實現具體函式的物件來解耦命令物件與接收物件。
我們現在有一個汽車管理類, 如下
(function() {
var carManager = {
// 獲取汽車的資訊
requestInfo: function(model, id) {
return "The information for " + model + " with ID " + id + " is foobar";
},
// 購買汽車
buyVehicle: function(model, id) {
return "You have successfully purchased Item " + id + ", a " + model;
},
// 組織車展
arrangeViewing: function(model, id) {
return (
"You have successfully booked a viewing of " +
model +
" ( " +
id +
" ) "
);
}
}
})();
// 呼叫方法
carManager.requestInfo( "Ferrari", "14523" );
carManager.buyVehicle( "Ford Mondeo", "54323" );
carManager.arrangeViewing("Ford Escort", "34232" );
複製程式碼
然而在一些情況下,我們並不想直接呼叫物件內部的方法。這樣會增加物件與物件間的依賴。現在我們來擴充套件一下這個CarManager, 使其能夠接受任何來自包括model和car ID 的CarManager物件的處理請求。根據命令模式的定義,我們希望實現如下這種功能的呼叫:
CarManager.execute({ commandType: "buyVehicle", operand1: 'Ford Escort', operand2: '453543' });
複製程式碼
根據這樣的需求,我們可以這樣實現CarManager.execute方法:
CarManager.execute = function (command) {
return CarManager[command.request](command.model, command.carID);
};
複製程式碼
改造以後,呼叫就簡單多了,如下呼叫都可以實現
CarManager.execute({ request: "arrangeViewing", model: 'Ferrari', carID: '145523' });
CarManager.execute({ request: "requestInfo", model: 'Ford Mondeo', carID: '543434' });
CarManager.execute({ request: "requestInfo", model: 'Ford Escort', carID: '543434' });
CarManager.execute({ request: "buyVehicle", model: 'Ford Escort', carID: '543434' });
複製程式碼
參考資料: