javascript設計模式與應用

夏天來嘍發表於2019-04-06

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)

命令模式(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' });
複製程式碼

參考資料:

相關文章