掌握前端5大常用設計模式,瞬間高大上
今天主要介紹一下我們平常會經常用到的設計模式,設計模式總的來說有23種,而設計模式在前端中又該怎么運用呢,接下來主要對比較前端中常見的設計模式做一個介紹。
設計模式的定義
設計模式是在面向對象軟件設計過程中針對特定問題的簡潔而優雅的解決方案。在不同的編程語言中,對設計模式的實現其實是可能會有區別的。比如java和javascript,在Java這種靜態編譯型語言中,無法動態地給已存在的對象添加職責,所以一般通過包裝類的方式來實現裝飾者模式。但在JavaScript這種動態解釋型語言中,給對象動態添加職責是再簡單不過的事情。這就造成了JavaScript語言的裝飾者模式不再關注于給對象動態添加職責,而是關注于給函數動態添加職責。本篇將介紹以下幾個比較常見的設計模式:
- 工廠模式
- 單例模式
- 代理模式
- 觀察者模式
- 策略模式
一、工廠模式
工廠模式是用來創建對象的一種最常用的設計模式,不暴露創建對象的具體邏輯,而是將將邏輯封裝在一個函數中,那么這個函數就可以被視為一個工廠,工廠模式根據抽象程度的不同可以分為:簡單工廠,工廠方法和抽象工廠,接下來,將對簡單工廠和工廠方法在JavaScript中的運用舉個簡單的例子:
1. 簡單工廠
簡單工廠模式又叫靜態工廠模式,由一個工廠對象決定創建某一種產品對象類的實例,主要用來創建同一類對象
比如說,在實際的項目中,我們常常需要根據用戶的權限來渲染不同的頁面,高級權限的用戶所擁有的頁面有些是無法被低級權限的用戶所查看,所以我們可以在不同權限等級用戶的構造函數中,保存該用戶能夠看到的頁面。
- let UserFactory = function (role) {
- function SuperAdmin() {
- this.name = "超級管理員",
- this.viewPage = ['首頁', '用戶管理', '訂單管理', '應用管理', '權限管理']
- }
- function Admin() {
- this.name = "管理員",
- this.viewPage = ['首頁', '訂單管理', '應用管理']
- }
- function NormalUser() {
- this.name = '普通用戶',
- this.viewPage = ['首頁', '訂單管理']
- }
- 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個而是30個或更多時,這個函數會成為一個龐大的超級函數,便得難以維護,簡單工廠只能作用于創建的對象數量較少,對象的創建邏輯不復雜時使用;
2. 工廠方法
工廠方法模式的本意是將實際創建對象的工作推遲到子類中,這樣核心類就變成了抽象類,但是在JavaScript中很難像傳統面向對象那樣去實現創建抽象類,所以在JavaScript中我們只需要參考它的核心思想即可,我們可以將工廠方法看作是一個實例化對象的工廠類
比如說上面的例子,我們用工廠方法可以這樣寫,工廠方法我們只把它看作是一個實例化對象的工廠,它只做實例化對象這一件事情,我們采用安全模式創建對象
- //安全模式創建的工廠方法函數
- let UserFactory = function(role) {
- if(this instanceof UserFactory) {
- var s = new this[role]();
- return s;
- } else {
- return new UserFactory(role);
- }
- }
- //工廠方法函數的原型中設置所有對象的構造函數
- UserFactory.prototype = {
- SuperAdmin: function() {
- this.name = "超級管理員",
- this.viewPage = ['首頁', '用戶管理', '訂單管理', '應用管理', '權限管理']
- },
- Admin: function() {
- this.name = "管理員",
- this.viewPage = ['首頁', '訂單管理', '應用管理']
- },
- NormalUser: function() {
- this.name = '普通用戶',
- this.viewPage = ['首頁', '訂單管理']
- }
- }
- //調用
- let superAdmin = UserFactory('SuperAdmin');
- let admin = UserFactory('Admin')
- let normalUser = UserFactory('NormalUser')
總結:在簡單工廠中,如果我們新增加一個用戶類型,需要修改兩個地方的代碼,一個是增加新的用戶構造函數,一個是在邏輯判斷中增加對新的用戶的判斷,而在抽象工廠方法中,我們只需要在UserFactory.prototype中添加就可以啦。
二、單例模式
定義:是保證一個類只有一個實例,并且提供一個訪問它的全局訪問點。
需求:一些對象我們往往只需要一個,比如線程池、全局緩存、瀏覽器中的window對象、登錄浮窗等。
實現:用一個變量標識當前是否已經為某個類創建過對象,如果是,則在下一次獲取這個類的實例時,直接返回之前創建的對象。
優點:
- 可以用來劃分命名空間,減少全局變量的數量
- 可以被實例化,且實例化一次,再次實例化生成的也是***個實例
下面舉個例子,在js中,我們可以使用閉包來創建實現這種模式:
- var single = (function(){
- var unique;
- function getInstance(){
- // 如果該實例存在,則直接返回,否則就對其實例化
- if( unique === undefined ){
- unique = new Construct();
- }
- return unique;
- }
- function Construct(){
- // ... 生成單例的構造函數的代碼
- }
- return {
- getInstance : getInstance
- }
- })();
總結:在上面的代碼中,我們可以使用single.getInstance來獲取到單例,并且每次調用均獲取到同一個單例,在我們平時的開發中,我們也經常會用到這種模式,比如當我們單擊登錄按鈕的時候,頁面中會出現一個登錄框,而這個浮窗是唯一的,無論單擊多少次登錄按鈕,這個浮窗只會被創建一次,因此這個登錄浮窗就適合用單例模式。
三、代理模式
代理模式主要是為其他對象提供一種代理以控制對這個對象的訪問,主要解決在直接訪問對象時帶來的問題,比如說:要訪問的對象在遠程的機器上,在面向對象系統中,有些對象由于某些原因(比如對象創建開銷很大,或者某些操作需要安全控制,或者需要進程外的訪問),直接訪問會給使用者或者系統結構帶來很多麻煩,我們可以在訪問此對象時加上一個對此對象的訪問層。
代理模式最基本的形式是對訪問進行控制,代理對象和另一個對象(本體)實現的是同樣的接口,實際上工作還是本體在做,它才是負責執行所分派的任務的那個對象或類,代理對象所做的不外乎節制對本體的訪問,代理對象并不會在另一對象的基礎上添加方法或修改其方法,也不會簡化那個對象的接口,它實現的接口與本體完全相同,所有對它進行的方法調用都會被傳遞給本體。
- (function(){
- // 示例代碼
- // 目標對象,是真正被代理的對象
- function Subject(){}
- Subject.prototype.request = function(){};
- /**
- * 代理對象
- * @param {Object} realSubject [持有被代理的具體的目標對象]
- */
- function Proxy(realSubject){
- this.realSubject = readSubject;
- }
- Proxy.prototype.request = function(){
- this.realSubject.request();
- };
- }());
總結:在上面的代碼中,Proxy可以控制對真正被代理對象的一個訪問,在代理模式中,比較常見的就是虛擬代理,虛擬代理用于控制對那種創建開銷很大的本體的訪問,它會把本體的實例化推遲到有方法被調用的時候,比如說,現在我們假設PublicLibrary的實例化很慢,不能在網頁加載的時候立即完成,我們可以為其創建一個虛擬代理,讓它把PublicLibrary的實例化推遲到必要的時候,比如說我們在前端中經常用到的圖片懶加載,就可以用虛擬代理;
四、觀察者模式
如果大家學過一些像vue,react這些框架,相信大家對觀察者模式一定很熟悉,現在很多mvvm框架都用到了觀察者模式這個思想,觀察者模式又叫做發布—訂閱模式,它定義對象間的一種一對多的依賴關系,當一個對象的狀態發生改變時,所有依賴于它的對象都將得到通知和更新,觀察者模式提供了一個訂閱模型,其中對象訂閱事件并在發生時得到通知,這種模式是事件驅動的編程基石,它有利益于良好的面向對象的設計
定義:對象間的一種一對多的依賴關系。
需求:當一個對象的狀態發生變化時,所有依賴于他的對象都將得到通知。
優點:時間上的解耦,對象之間的解耦。
實現:
- 指定好誰充當發布者;
- 給發布者添加一個緩存列表,用于存放回調函數以便通知訂閱者;
- 發布消息的時候,發布者會遍歷這個緩存列表,依次觸發里面存放的訂閱者回調函數。
下面舉個例子,比如我們給頁面中的一個dom節點綁定一個事件,其實就可以看做是一種觀察者模式:
- document.body.addEventListener("click", function() {
- alert("Hello World")
- },false )
- document.body.click() //模擬用戶點擊
總結:在上面的例子中,需要監聽用戶點擊 document.body 的動作,但是我們是沒辦法預知用戶將在什么時候點擊的,因此我們訂閱了 document.body 的 click 事件,當 body 節點被點擊時,body 節點便會向訂閱者發布 "Hello World" 消息。
五、策略模式
策略模式指的是定義一些列的算法,把他們一個個封裝起來,目的就是將算法的使用與算法的實現分離開來,避免多重判斷條件,更具有擴展性。
下面也是舉個例子,現在超市有活動,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; //其他都全價
- }
- }
在上面的代碼中,我們需要很多個判斷,如果有很多優惠,我們又需要添加很多判斷,這里已經違背了剛才說的設計模式的六大原則中的開閉原則了,如果使用策略模式,我們的代碼可以這樣寫:
- // 對于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
總結:在上面的代碼中,通過策略模式,使得客戶的折扣與算法解藕,又使得修改跟擴展能獨立的進行,不影到客戶端或其他算法的使用。
當我們的代碼中有很多個判斷分支,每一個條件分支都會引起該“類”的特定行為以不同的方式作出改變,這個時候就可以使用策略模式,可以改進我們代碼的質量,也更好的可以進行單元測試。