1. 程式人生 > 實用技巧 >JavaScript常用設計模式

JavaScript常用設計模式

設計模式的定義是:在面向物件軟體設計過程中針對特定問題的簡潔而優雅的解決方案。

建立型(Creational Patterns)

單例模式(Singleton Pattern)

保證一個類僅有一個例項,並提供一個訪問它的全域性訪問點。

// 惰性單例
const getSingle = (fn) => {
  let result;
  return function () {
    return result || (result = fn.apply(this, arguments));
  };
};

// 凍結物件
Object.freeze(instance);

原型模式(Prototype Pattern)

用原型例項指定建立物件的種類,並且通過拷貝這些原型建立新的物件。
ECMAScript 5提供了Object.create方法,可以用來克隆物件。

// Object.create
var proto = {};
var properties = {
  name: {
    value: 'name',
    writable: true,
    enumerable: false,
    configurable: false,
  },
};
var obj = Object.create(proto, properties);

// 方法繼承
Child.prototype = new Parent()

工廠模式(Factory Pattern)

在建立物件時不暴露建立邏輯,而是通過使用一個共同的介面來指向新建立的物件,用工廠方法代替new操作。

// 簡單工廠
const UserFactory = function (role) {
  function User(opt) {
    this.name = opt.name;
  }

  switch (role) {
    case 'admin':
      return new User({ name: '管理員');
      break;
    default:
      throw new Error('引數錯誤')
  }
}
// 工廠方法(安全模式)
const UserFactory = function(role) {
  if(this instanceof UserFactory) {
    return new this[role]();
  } else {
    return new UserFactory(role);
  }
}
UserFactory.prototype = {}

let admin = UserFactory('Admin') 

結構型(Structural Patterns)

代理模式(Proxy Pattern)

為一個物件提供一個代用品或佔位符,以便控制對它的訪問。
保護代理:用於控制不同許可權的物件對目標物件的訪問;
虛擬代理:可應用於圖片懶載入、惰性載入、合併http請求等;
快取代理:可應用於快取ajax非同步請求資料、計算乘積等;

// 快取代理工廠
const createProxy = (fn) => {
  const cache = [];
  return function () {
    const args = [].join.call(arguments, ',');
    if (args in cache) {
      return cache[args];
    }
    return (cache[args] = fn.apply(this, arguments));
  };
};

介面卡模式(Adapter Pattern)

解決兩個軟體實體間的介面、方法不相容的問題。

// 介面適配
const user1 = { name: 'user1' };
const user2 = { username: 'user2' };
function getName(param) {
  return param.name
}
function adapter(param) {
  return { name: param.username };
}
getName(user1);
getName(adapter(user2));

相似模式區分
裝飾者模式:為了給物件增加功能,常常形成一條長的裝飾鏈;
介面卡模式:用來解決兩個已有介面之間不匹配的問題,通常只包裝一次;
外觀模式:定義了一個新的介面;

裝飾器模式(Decorator Pattern)

在不改變物件自身的基礎上,在程式執行期間給物件動態地新增職責。

const user = {
  name: 'username',
  getName() {
    return this.name;
  },
};

function addDecorator(user) {
  const prevName = user.getName();
  user.getName = function () {
    return prevName + ' with decorator';
  };
}

用AOP裝飾函式

Function.prototype.before = function (beforefn) {
  var self = this;
  return function () {
    beforefn.apply(this, arguments);
    return self.apply(this, arguments);
  };
};

Function.prototype.after = function (afterfn) {
  var self = this;
  return function () {
    var ret = self.apply(this, arguments);
    afterfn.apply(this, arguments);
    return ret;
  };
};

var before = function (fn, beforefn) {
  return function () {
    beforefn.apply(this, arguments);
    return fn.apply(this, arguments);
  };
};

外觀模式(Facade Pattern)

外觀模式為子系統中的一組介面提供了一個一致的介面,此模組定義了一個高層介面,這個介面使得這一子系統更加容易使用,他可以將一些複雜的操作封裝起來,並建立一個簡單的介面用於呼叫。
外觀模式的作用是對客戶遮蔽一組子系統的複雜性。外觀模式對客戶提供一個簡單易用的高層介面,高層介面會把客戶的請求轉發給子系統來完成具體的功能實現。大多數客戶都可以通過請求外觀介面來達到訪問子系統的目的。但在一段使用了外觀模式的程式中,請求外觀並不是強制的。如果外觀不能滿足客戶的個性化需求,那麼客戶也可以選擇越過外觀來直接訪問子系統。

  • 為一組子系統提供一個簡單便利的訪問入口
  • 隔離客戶與複雜子系統之間的聯絡,客戶不用去了解子系統的細節
function addEvent(dom, type, fn) {
  if (dom.addEventListener) {
    dom.addEventListener(type, fn, false);
  } else if (dom.attachEvent) {
    dom.attachEvent('on' + type, fn);
  } else {
    dom['on' + type] = fn;
  }
}

組合模式(Composite Pattern)

組合模式將物件組合成樹形結構,以表示“部分-整體”的層次結構。
1)組合模式不是父子關係,它們能夠合作的關鍵是擁有相同的介面;
2)對葉物件操作的一致性,要對每個目標物件都實行同樣的操作;
3)可用中介者模式處理雙向對映關係,例如一個子節點同時在不同的父節點中存在(組織架構);
4)可用職責鏈模式提高組合模式效能,通過設定鏈條避免每次都遍歷整個樹;
何時使用組合模式

  • 表示物件的部分-整體層次結構。
  • 客戶希望統一對待樹中的所有物件。

享元模式(Flyweight Pattern)

享元模式是一種用於效能優化的模式,核心是運用共享技術來有效支援大量細粒度的物件。
享元模式要求將物件的屬性劃分為內部狀態與外部狀態(狀態在這裡通常指屬性),目標是儘量減少共享物件的數量。
1)內部狀態儲存於物件內部。
2)內部狀態可以被一些物件共享。
3)內部狀態獨立於具體的場景,通常不會改變。
4)外部狀態取決於具體的場景,並根據場景而變化,外部狀態不能被共享。
使用場景

  • 一個程式中使用了大量的相似物件
  • 由於使用了大量的物件,造成了很大的記憶體開銷
  • 物件的大多數狀態都可以變為外部狀態
  • 剝離出物件的外部狀態之後,可以使用相對較少的共享物件取代大量物件

物件池
物件池維護一個裝載空閒物件的池子,如果需要物件的時候,不是直接 new ,而是轉從物件池裡獲取。如果物件池裡沒有空閒物件,則建立一個新的物件,當獲取出的物件完成它的職責之後, 再進入池子等待被下次獲取。

// 物件池快取物件
class colorFactory {
  constructor(name) {
    this.colors = {};
  }
  create(name) {
    let color = this.colors[name];
    if (color) return color;
    this.colors[name] = new Color(name);
    return this.colors[name];
  }
};

行為型(Behavioral Patterns)

策略模式(Strategy Pattern)

定義一系列的演算法,把它們一個個封裝起來,並且使它們可以相互替換。

const strategies = {
  strategy: () => {},
};
const useStrategy = (fn, args) => fn(args);
useStrategy(strategies.strategy);

模板方法模式(Template Pattern)

模板方法是基於繼承的設計模式,通過封裝變化提高系統擴充套件性
1)抽象父類,封裝子類的演算法框架,包括實現一些公共方法以及封裝子類中所有方法的執行順序;
2)具體的實現子類,子類通過繼承抽象父類,繼承整個演算法結構,並且可以選擇重寫父類的方法;

var Beverage = function (param) {
  var boilWater = function () {
    console.log('把水煮沸');
  };
  var brew = param.brew || function () {
    throw new Error('必須傳遞brew方法');
  };
  var pourInCup = param.pourInCup || function () {
    throw new Error('必須傳遞pourInCup方法');
  };
  var addCondiments = param.addCondiments || function () {
    throw new Error('必須傳遞addCondiments方法');
  };
  var wantsCondiments = param.wantsCondiments || function () {
    return true;
  };

  var F = function () {};
  F.prototype.init = function () {
    boilWater();
    brew();
    pourInCup();
    // 鉤子方法 hook
    if (wantsCondiments()) {
      addCondiments();
    }
  };
  return F;
};

var Tea = Beverage({
  brew: function () {
    console.log('用沸水浸泡茶葉');
  },
  pourInCup: function () {
    console.log('把茶倒進杯子');
  },
  addCondiments: function () {
    console.log('加檸檬');
  },
  wantsCondiments: function () {
    return window.confirm('請問需要調料嗎?');
  },
});

var tea = new Tea();
tea.init();

使用鉤子方法(hook)是隔離變化的一種常見手段。在父類中容易變化的地方放置鉤子,鉤子可以有一個預設的實現,究竟要不要“掛鉤”,這由子類自行決定。

迭代器模式(Iterator Pattern)

提供一種方法順序訪問一個聚合物件中的各個元素,而又不需要暴露該物件的內部表示。
內部迭代器:內部已經定義好了迭代規則,它完全接手整個迭代過程,外部只需要一次初始呼叫;
外部迭代器:必須顯式地請求迭代下一個元素;

// 內部迭代器
const each = (arr, callback) => {
  for (let i = 0, len = arr.length; i < len; i++) {
    // callback 的執行結果返回false,提前終止迭代
    if (callback.call(arr[i], i, arr[i]) === false) {
      break;
    }
  }
};
// 外部迭代器
const Iterator = (obj) => {
  let curr = 0;
  const next = () => {
    curr += 1;
  };
  const isDone = () => curr === obj.length;
  const getCurrItem = () => obj[curr];
  return { next, isDone, getCurrItem, length: obj.length };
};

釋出-訂閱模式(Publish-Subscribe Pattern)

它定義物件間的一種一對多的依賴關係,當一個物件的狀態發生改變時,所有依賴於它的物件都將得到通知。

var pubsub = (function () {
  var clientList = {};
  return {
    subscribe(key, fn) {
      if (!clientList[key]) {
        clientList[key] = [];
      }
      clientList[key].push(fn);
    },
    publish() {
      var key = Array.prototype.shift.call(arguments),
        fns = clientList[key];
      if (!fns || fns.length === 0) {
        return false;
      }
      for (var i = 0, fn; (fn = fns[i++]); ) {
        fn.apply(this, arguments);
      }
    },
    unsubscribe(key, fn) {
      var fns = clientList[key];
      if (!fns) {
        return false;
      }
      if (!fn) {
        fns && (fns.length = 0);
      } else {
        for (var i = fns.length - 1; i >= 0; i--) {
          fns[i] === fn && fns.splice(i, 1);
        }
      }
    },
  };
})();

釋出-訂閱模式和觀察者模式的不同
釋出-訂閱模式:訂閱者(Subscriber)把自己想訂閱的事件註冊(Subscribe)到排程中心(Event Channel),當釋出者(Publisher)釋出該事件(Publish Event)到排程中心,也就是該事件觸發時,由排程中心統一排程(Fire Event)訂閱者註冊到排程中心的處理程式碼。
觀察者模式:觀察者(Observer)直接訂閱(Subscribe)主題(Subject),而當主題被啟用的時候,會觸發(Fire Event)觀察者裡的事件。

命令模式(Command Pattern)

將方法的呼叫、請求或者操作封裝到一個單獨的物件中,給我們酌情執行同時引數化和傳遞方法呼叫的能力。
命令(command)指的是一個執行某些特定事情的指令。
巨集命令是一組命令的集合,通過執行巨集命令的方式,可以依次執行一批命令

const RefreshMenuBarCommand = (receiver) => ({
  execute() {
    receiver.refresh();
  },
});
const setCommand = (button, command) => {
  button.onclick = function () {
    command.execute();
  };
};
setCommand(button, RefreshMenuBarCommand(MenuBar));

狀態模式(State Pattern)

允許一個物件在其內部狀態改變時改變它的行為,物件看起來似乎修改了它的類。
1)將狀態封裝成獨立的類,並將請求委託給當前的狀態物件,當物件的內部狀態改變時,會帶來不同的行為變化;
2)從客戶的角度來看,我們使用的物件,在不同的狀態下具有截然不同的行為,這個物件看起來是從不同的類中例項化而來的,實際上這是使用了委託的效果。

var Light = function () {
  this.currState = FSM.off;
  this.button = null;
};

Light.prototype.init = function () {
  var self = this,
    button = document.createElement('button');
  button.innerHTML = '已關燈';
  this.button = document.body.appendChild(button);

  this.button.onclick = function () {
    self.currState.buttonWasPressed.call(self); // 把請求委託給FSM狀態機
  };
};

var FSM = {
  off: {
    buttonWasPressed: function () {
      console.log('關燈');
      this.currState = FSM.on;
      this.button.innerHTML = '下一次按我是開燈';
    },
  },
  on: {
    buttonWasPressed: function () {
      console.log('開燈');
      this.currState = FSM.off;
      this.button.innerHTML = '下一次按我是關燈';
    },
  },
};

var light = new Light();
light.init();

狀態模式可用來優化條件分支語句、含有大量狀態且行為隨狀態改變而改變的場景,關鍵是區分事物內部的狀態,事物內部狀態的改變往往會帶來事物的行為改變。
狀態模式和策略模式
共同點:都封裝了一系列的演算法或者行為,都有一個上下文、一些策略或者狀態類,上下文把請求委託給這些類來執行;
不同點:策略模式中的各個策略類是平等又平行的,他們之間沒有任何聯絡,演算法切換是使用者主動完成的;而在狀態模式中,狀態和狀態對應的行為是早已被封裝好的,狀態之間的切換也早被規定完成,“改變行為”這件事情發生在狀態模式內部,客戶不需要了解這些細節;

中介者模式(Mediator Pattern)

中介者模式的作用就是解除物件與物件之間的緊耦合關係。以中介者和物件之間的一對多關係取代了物件之間的網狀多對多關係。各個物件只需關注自身功能的實現,物件之間的互動關係交給了中介者物件來實現和維護。


var goods = {
  'red|32G': 3,
  'red|16G': 0,
  'blue|32G': 1,
  'blue|16G': 6,
};
var colorSelect = document.getElementById('colorSelect'),
  memorySelect = document.getElementById('memorySelect'),
  numberInput = document.getElementById('numberInput');
  
var mediator = (function () {
  var colorInfo = document.getElementById('colorInfo'),
    memoryInfo = document.getElementById('memoryInfo'),
    numberInfo = document.getElementById('numberInfo'),
    nextBtn = document.getElementById('nextBtn');
  return {
    changed: function (obj) {
      var color = colorSelect.value,
        memory = memorySelect.value,
        number = numberInput.value,
        stock = goods[color + '|' + memory];

      if (obj === colorSelect) {
        colorInfo.innerHTML = color;
      } else if (obj === memorySelect) {
        memoryInfo.innerHTML = memory;
      } else if (obj === numberInput) {
        numberInfo.innerHTML = number;
      }

      if (!color) {
        nextBtn.disabled = true;
        nextBtn.innerHTML = '請選擇手機顏色';
        return;
      }
      if (!memory) {
        nextBtn.disabled = true;
        nextBtn.innerHTML = '請選擇記憶體大小';
        return;
      }
      if (Number.isInteger(number - 0) && number > 0) {
        nextBtn.disabled = true;
        nextBtn.innerHTML = '請輸入正確的購買數量';
        return;
      }
      if (number > stock) {
        nextBtn.disabled = true;
        nextBtn.innerHTML = '庫存不足';
        return;
      }

      nextBtn.disabled = false;
      nextBtn.innerHTML = '放入購物車';
    },
  };
})();

colorSelect.onchange = function () {
  mediator.changed(this);
};
memorySelect.onchange = function () {
  mediator.changed(this);
};
numberInput.oninput = function () {
  mediator.changed(this);
};

職責鏈模式(Chain of Responsibility Pattern)

使多個物件都有機會處理請求,從而避免請求的傳送者和接收者之間的耦合關係,將這些物件連成一條鏈,並沿著這條鏈傳遞該請求,直到有一個物件處理它為止。

var Chain = function (fn) {
  this.fn = fn;
  this.successor = null;
};
Chain.prototype.setNextSuccessor = function (successor) {
  return (this.successor = successor);
};
Chain.prototype.passRequest = function () {
  var ret = this.fn.apply(this, arguments);
  if (ret === 'nextSuccessor') {
    return this.next();
  }
  return ret;
};
Chain.prototype.next = function () {
  return (
    this.successor &&
    this.successor.passRequest.apply(this.successor, arguments)
  );
};