JavaScript設計模式學習筆記

《JavaScript設計模式與開發實踐》14種設計模式學習筆記。

單例模式

保證一個類僅有一個實例,並提供一個全局訪問點。

// 構造函數
var Singleton = function (name) {
  this.name = name;
}
Singleton.prototype.getName = function () {
  alert(this.name);
}

// 方案1 綁定到構造函數上
Singleton.instance = null;
Singleton.getInstance = function (name) {
  if (!this.instance) {
    this.instance = new Singleton(name);
  }
  return this.instance;
}

// 方案2 閉包
Singleton.getInstance = (function () {
  var instance = null;
  return function (name) {
    if (!instance) {
      instance = new Singleton(name);
    }
    return instance;
  }
})()

惰性單例

需要的時候才創建對象實例

var getSingle = function (fn) {
  var result;
  return function () {
    return result || (result = fn.apply(this, arguments));
  }
}

例如一個Modal彈框,頁面加載時不需要創建,按鈕點擊後纔會被創建。以後再點擊按鈕不需要創建新的Modal。

var createModal = function () {
  var div = document.createElement('div');
  div.style.display = 'none';
  document.body.append(div);
  return div;
}

createSingleModal = getSingle(createModal);

btn.click = function () {
  var modal = createSingleModal();
  modal.style.display = 'block';
}

策略模式

定義一系列算法,把它們一個個封裝起來,並且使它們可以互相替換。在實際開發中,我們通常會把算法的含義擴散開來,使用策略模式也可以用來封裝一系列目標一致的‘業務規則’。

例如計算年終獎:

var strategies = {
  "S": function (salary) {
    return salary * 4;
  },
  "A": function (salary) {
    return salary * 3;
  },
  "B": function (salary) {
    return salary * 2;
  },
}
var calculateBonus = function (level, salary) {
  return strategies[level](salary);
}

calculateBonus('S', 20000);  // 80000
calculateBonus('A', 10000);  // 30000

代理模式

顧名思義,代理。

當直接訪問本體不方便或者不符合需要時,爲這個本體提供一個替代者。

虛擬代理吧一些開銷很大的對象,延遲到真正需要他的時候纔去創建。

虛擬代理實現圖片預加載

var myImage = (function () {
  var imgNode = document.createElement('img');
  document.body.append(imgNode);

  return function (src) {
    imgNode.src = src;
  }
})()

// 代理 myImage,實現預加載
var proxyImage = (function () {
  var img = new Image();

  img.onload = function () {
    myImage(this.src);
  }

  return function (src) {
    myImage('./loading.gif');
    img.src = src;
  }
})()

proxyImage('http://xxx.10M.png');

JavaScript 開發中最常用的是虛擬代理和緩存代理。


迭代器模式

迭代器模式是指提供一種方法順序訪問一個聚合對象中的各個元素,而又不需要暴露該對象的內部表示。

實現迭代器(內部迭代器)

var each = function (arr, callback) {
  for (var i = 0; i < arr.length; i++) {
    if (callback.call(arr[i], arr[i], i) === false) {
      break;
    }
  }
}

each([1, 2, 3, 4, 5], function (item, index) {
  if (index > 3) {
    return false;
  }
  console.log(item, index);
})

外部迭代器

var Iterator = function (obj) {
  var current = 0;
  var next = function () {
    current += 1;
  };

  var isDone = function () {
    return current >= obj.length;
  };

  var getCurrItem = function () {
    return obj[current];
  };

  return {
    next: next,
    isDone: isDone,
    getCurrItem: getCurrItem,
    length: obj.length,
  }
}

發佈-訂閱模式

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

var event = {
  clientList: {},
  listen: function (key, fn) {
    if (!clientList[key]) {
      clientList[key] = [];
    }

    clientList[key].push(fn);
  },
  trigger: function () {
    var key = Array.prototype.shift.apply(arguments),
      fns = this.clientList[key];

    if (!fns || fns.length === 0) {
      return false;
    }

    for (var i = 0; i < fns.length; i++) {
      fns[i].apply(this, arguments);
    }
  },
  remove: function (key, fn) {
    var fns = this.clientList[key];

    if (!fns || fns.length === 0) {
      return false;
    }

    if (!fn) {
      this.clientList = [];
    } else {
      for (var i = 0; i < fns.length; i++) {
        if (fn === fns[i]) {
          fns.splice(i, 1);
        }
      }
    }

  },
}

DOM事件也是發佈-訂閱模式。


命令模式

命令模式把代碼封裝成命令,目的解藕。

命令模式有 接收者receiver,執行方法execute;execute 去執行 receiver.xxx()。

var setCommand = function (button, command) {
  button.onClick = function () {
    command.execute();
  }
}

var MenuBar = {
  refresh: function () {
    console.log('刷新菜單')
  }
}

var RefreshMenuBarCommand = function (receiver) {
  this.receiver = receiver;
}

RefreshMenuBarCommand.prototype.execute = function () {
  this.receiver.refresh();
}

var refreshMenuBarCommand = new RefreshMenuBarCommand(MenuBar);

setCommand(btn, refreshMenuBarCommand);

JavaScript 中的命令模式未必要使用面向對象:

var setCommand = function (button, command) {
  button.onClick = function () {
    command.execute();
  }
}

var MenuBar = {
  refresh: function () {
    console.log('刷新菜單');
  }
}

var RefreshMenuBarCommand = function (receiver) {
  return {
    execute: function () {
      receiver.refresh();
    }
  }
}

var refreshMenuBarCommand = RefreshMenuBarCommand(MenuBar);

setCommand(btn, refreshMenuBarCommand);

JavaScript 中命令模式還可以不需要接收者:

var refreshMenuBarCommand = {
  execute: function () {
    console.log('刷新菜單');
  }
};

這樣看起來代碼結構和策略模式非常相近,但他們的意圖不同,策略模式的策略對象的目標總是一致的,命令模式的目標更具散發性,命令模式還可以完成撤銷,排隊等功能。


組合模式

組合模式中有兩個名詞:組合對象,葉對象。

結構如圖

組合模式的例子-掃描文件夾

// Folder
var Folder = function (name) {
  this.name = name;
  this.parent = null;
  this.files = [];
};

Folder.prototype.add = function (file) {
  file.parent = this;
  this.files.push(file);
};

Folder.prototype.remove = function () {
  if (!this.parent) {
    return;
  }

  for (var files = this.parent.files, i = 0; i < files.length; i++) {
    var file = files[i];
    if (file === this) {
      files.splice(i, 1);
      break;
    }
  }
};

Folder.prototype.scan = function () {
  console.log('開始掃描文件夾:' + this.name);
  for (var i = 0; i < this.files.length; i++) {
    var file = this.files[i];
    file.scan();
  }
};

// File
var File = function (name) {
  this.name = name;
  this.parent = null;
};

File.prototype.add = function () {
  throw new Error('文件夾下面不能在添加文件');
};

Folder.prototype.remove = function () {
  if (!this.parent) {
    return;
  }

  for (var files = this.parent.files, i = 0; i < files.length; i++) {
    var file = files[i];
    if (file === this) {
      files.splice(i, 1);
      break;
    }
  }
};

File.prototype.scan = function () {
  console.log('開始掃描文件:' + this.name);
};

// 測試
var folder = new Folder('學習資料');
var folder1 = new Folder('JavaScript');
folder1.add(new File('JavaScript設計模式與開發實踐'));
folder.add(folder1);
folder.add(new File('深入淺出Node.js'));

console.log('第一次掃描');
folder.scan();


folder1.remove();
console.log('第二次掃描');
folder.scan();

模板方法模式

模板方法模式是一種只需使用繼承就可以實現的非常簡單的模式。

模板方法模式由兩部分結構組成,第一部分是抽象父類,第二部分是具體的實現子類。

假如我們有一些平行的子類,各個子類之間有一些相同的行爲,也有一些不同的行爲。如果 相同和不同的行爲都混合在各個子類的實現中,說明這些相同的行爲會在各個子類中重複出現。 但實際上,相同的行爲可以被搬移到另外一個單一的地方,模板方法模式就是爲解決這個問題而 生的。在模板方法模式中,子類實現中的相同部分被上移到父類中,而將不同的部分留待子類來 實現。這也很好地體現了泛化的思想。

例子:咖啡與茶

先泡一杯咖啡

  1. 把水煮沸
  2. 用沸水沖泡咖啡
  3. 把咖啡倒進杯子
  4. 加糖和牛奶
var Coffee = function () { };
Coffee.prototype.boilWater = function () {
  console.log('把水煮沸');
};
Coffee.prototype.brewCoffeeGriends = function () {
  console.log('用沸水沖泡咖啡');
};
Coffee.prototype.pourInCup = function () {
  console.log('把咖啡倒進杯子');
};
Coffee.prototype.addSugarAndMilk = function () {
  console.log('加糖和牛奶');
};
Coffee.prototype.init = function () {
  this.boilWater();
  this.brewCoffeeGriends();
  this.pourInCup();
  this.addSugarAndMilk();
};
var coffee = new Coffee();
coffee.init();

泡一壺茶

  1. 把水煮沸
  2. 用沸水浸泡茶葉
  3. 把茶水倒進杯子
  4. 加檸檬
var Tea = function () { };
Tea.prototype.boilWater = function () {
  console.log('把水煮沸');
};
Tea.prototype.steepTeaBag = function () {
  console.log('用沸水浸泡茶葉');
};
Tea.prototype.pourInCup = function () {
  console.log('把茶水倒進杯子');
};
Tea.prototype.addLemon = function () {
  console.log('加檸檬');
};
Tea.prototype.init = function () {
  this.boilWater();
  this.steepTeaBag();
  this.pourInCup();
  this.addLemon();
};
var tea = new Tea();
tea.init();

分離出共同點

  1. 把水煮沸
  2. 用沸水沖泡飲料
  3. 把飲料倒進杯子
  4. 加調料
var Beverage = function () { };
Beverage.prototype.boilWater = function () {
  console.log('把水煮沸');
};
Beverage.prototype.brew = function () { };  // 空方法,應該由子類重寫
Beverage.prototype.pourInCup = function () { };  // 空方法,應該由子類重寫
Beverage.prototype.addCondiments = function () { };  // 空方法,應該由子類重寫
Beverage.prototype.init = function () {
  this.boilWater();
  this.brew();
  this.pourInCup();
  this.addCondiments();
};

創建Coffee子類

var Coffee = function () { };
Coffee.prototype = new Beverage();
Coffee.prototype.brew = function () {
  console.log('用沸水沖泡咖啡');
};
Coffee.prototype.pourInCup = function () {
  console.log('把咖啡倒進杯子');
};
Coffee.prototype.addCondiments = function () {
  console.log('加糖和牛奶');
};
var coffee = new Coffee();
coffee.init();

創建Tea子類

var Tea = function () { };
Tea.prototype = new Beverage();
Tea.prototype.brew = function () {
  console.log('用沸水浸泡茶葉');
};
Tea.prototype.pourInCup = function () {
  console.log('把茶倒進杯子');
};
Tea.prototype.addCondiments = function () {
  console.log('加檸檬');
};
var tea = new Tea();
tea.init();

Beverage.prototype.init 被稱爲模板方法的原因是,該方法中封裝了子類的算法框架,它作 爲一個算法的模板,指導子類以何種順序去執行哪些方法。在 Beverage.prototype.init 方法中, 算法內的每一個步驟都清楚地展示在我們眼前。


享元模式

享元模式是一種用於性能優化的模式,核心是運用共享技術來有效支持大量細粒度的對象。

內部狀態 儲存於共享對象內部,而 外部狀態 儲存於共享對象的外部,在必要時被傳入共享對象來組裝成一個完整的對象。

上傳文件的例子

下面代碼同時選擇 2000 個文件時,會 new 2000 個 upload 對象。

var id = 0;

window.startUpload = function (uploadType, files) {  // uploadType 區分是控件還是 flash
  for (var i = 0; i < files.length; i++) {
    var file = files[i];
    var uploadObj = new Upload(uploadType, file.fileName, file.fileSize);
    uploadObj.init(id++);
  }
};

var Upload = function (uploadType, fileName, fileSize) {
  this.uploadType = uploadType;
  this.fileName = fileName;
  this.fileSize = fileSize;
  this.dom = null;
};

Upload.prototype.init = function (id) {
  var that = this;
  this.id = id;
  this.dom = document.createElement('div');
  this.dom.innerHTML = '<span>文件名稱:' + this.fileName + ',文件大小:' + this.fileSize + '</span>' + '<button class="delFile">刪除</button>';
  this.dom.querySelector('.delFile').onclick = function () {
    that.delFile();
  }
  document.body.appendChild(this.dom);
};

Upload.prototype.delFile = function () {
  if (this.fileSize < 3000) {
    return this.dom.parentNode.removeChild(this.dom);
  }

  if (window.confirm('確定刪除該文件嗎?' + this.fileName)) {
    return this.dom.parentNode.removeChild(this.dom);
  }
};

// 上傳
startUpload('plugin', [
  { fileName: '1.txt', fileSize: 1000 },
  { fileName: '2.txt', fileSize: 3000 },
  { fileName: '3.txt', fileSize: 5000 },
]);
startUpload('flash', [
  { fileName: '4.txt', fileSize: 1000 },
  { fileName: '5.txt', fileSize: 3000 },
  { fileName: '6.txt', fileSize: 5000 },
]);

享元模式重構文件上傳

var Upload = function (uploadType) {
  this.uploadType = uploadType;
};

Upload.prototype.delFile = function (id) {
  uploadManager.setExternalState(id, this);

  if (this.fileSize < 3000) {
    return this.dom.parentNode.removeChild(this.dom);
  }

  if (window.confirm('確定刪除該文件嗎?' + this.fileName)) {
    return this.dom.parentNode.removeChild(this.dom);
  }
};

var UploadFactory = (function () {
  var createdFlyWeightObjs = {};

  return {
    create: function (uploadType) {
      if (createdFlyWeightObjs[uploadType]) {
        return createdFlyWeightObjs[uploadType];
      }

      return createdFlyWeightObjs[uploadType] = new Upload(uploadType);
    }
  }
})();

var uploadManager = (function () {
  var uploadDatabase = {};

  return {
    add: function (id, uploadType, fileName, fileSize) {
      var flyWeightObj = UploadFactory.create(uploadType);

      var dom = document.createElement('div');
      dom.innerHTML = '<span>文件名稱:' + fileName + ',文件大小:' + fileSize + '</span>' + '<button class="delFile">刪除</button>';

      dom.querySelector('.delFile').onclick = function () {
        flyWeightObj.delFile(id);
      };

      document.body.appendChild(dom);

      uploadDatabase[id] = {
        fileName: fileName,
        fileSize: fileSize,
        dom: dom,
      };

      return flyWeightObj;
    },
    setExternalState: function (id, flyWeightObj) {
      var uploadData = uploadDatabase[id];
      for (var key in uploadData) {
        flyWeightObj[key] = uploadData[key];
      }
    }
  }
})();

var id = 0;

window.startUpload = function (uploadType, files) {
  for (var i = 0; i < files.length; i++) {
    var file = files[i];
    var uploadObj = uploadManager.add(++this.id, uploadType, file.fileName, file.fileSize);
  }
};

// 上傳
startUpload('plugin', [
  { fileName: '1.txt', fileSize: 1000 },
  { fileName: '2.txt', fileSize: 3000 },
  { fileName: '3.txt', fileSize: 5000 },
]);
startUpload('flash', [
  { fileName: '4.txt', fileSize: 1000 },
  { fileName: '5.txt', fileSize: 3000 },
  { fileName: '6.txt', fileSize: 5000 },
]);

享元模式重構後,無論上傳多少次,Upload 對象(內部狀態)的數量一直是 2。


職責鏈模式

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

var order500 = function (orderType, pay, stock) {
  if (orderType === 1 && pay === true) {
    console.log('500 元定金預購,得到 100 優惠券');
  } else {
    return 'nextSuccessor';
  }
};

var order200 = function (orderType, pay, stock) {
  if (orderType === 2 && pay === true) {
    console.log('200 元定金預購,得到 50 優惠券');
  } else {
    return 'nextSuccessor';
  }
};

var orderNormal = function (orderType, pay, stock) {
  if (stock > 0) {
    console.log('普通購買,無優惠券');
  } else {
    console.log('手機庫存不足');
  }
};

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.successor && this.successor.passRequest.apply(this.successor, arguments);
  }
  return ret;
};

var chainOrder500 = new Chain(order500);
var chainOrder200 = new Chain(order200);
var chainOrderNormal = new Chain(orderNormal);

chainOrder500.setNextSuccessor(chainOrder200);
chainOrder200.setNextSuccessor(chainOrderNormal);

// 使用
chainOrder500.passRequest(1, true, 500); // 輸出:500 元定金預購,得到 100 優惠券
chainOrder500.passRequest(2, true, 500); // 輸出:200 元定金預購,得到 50 優惠券
chainOrder500.passRequest(3, true, 500); // 輸出:普通購買,無優惠券
chainOrder500.passRequest(1, false, 0);  // 輸出:手機庫存不足

用 AOP 實現指責鏈

Function.prototype.after = function (fn) {
  var self = this;
  return function () {
    var ret = self.apply(this, arguments);
    if (ret === 'nextSuccessor') {
      return fn.apply(this, arguments);
    }

    return ret;
  }
};

var order = order500.after(order200).after(orderNormal);

order(1, true, 500);   // 輸出:500 元定金預購,得到 100 優惠券
order(2, true, 500);   // 輸出:200 元定金預購,得到 50 優惠券
order(1, false, 500);  // 輸出:普通購買,無優惠券

中介者模式

中介者模式的作用就是解除對象與對象之間的緊耦合關係。增加一箇中介者對象後,所有的 相關對象都通過中介者對象來通信,而不是互相引用,所以當一個對象發生改變時,只需要通知 中介者對象即可。

變成了


裝飾者模式

爲對象動態加入行爲。裝飾者模式經常會形成一條長長的裝飾鏈。

面向對象裝飾者模式

// 原始的飛機類
var Plane = function () { }
Plane.prototype.fire = function () {
  console.log('發射普通子彈');
};

// 接下來增加兩個裝飾類,分別是導彈和原子彈:
var MissileDecorator = function (plane) {
  this.plane = plane;
};
MissileDecorator.prototype.fire = function () {
  this.plane.fire();
  console.log('發射導彈');
};
var AtomDecorator = function (plane) {
  this.plane = plane;
};
AtomDecorator.prototype.fire = function () {
  this.plane.fire();
  console.log('發射原子彈');
};

// 運行
var plane = new Plane();
plane = new MissileDecorator(plane);
plane = new AtomDecorator(plane);
plane.fire();
// 分別輸出: 發射普通子彈、發射導彈、發射原子彈

JavaScript 裝飾者模式

var plane = {
  fire: function () {
    console.log('發射普通子彈');
  }
};

var missileDecorator = function () {
  console.log('發射導彈');
};

var atomDecorator = function () {
  console.log('發射原子彈');
};

var fire1 = plane.fire;
plane.fire = function () {
  fire1();
  missileDecorator();
};

var fire2 = plane.fire;
plane.fire = function () {
  fire2();
  atomDecorator();
};

plane.fire();
// 分別輸出: 發射普通子彈、發射導彈、發射原子彈

狀態模式

狀態模式的關鍵是區分事物內部的狀態,事物內部狀態的改變往往會帶來事物的行爲改變。

例子:電燈的 弱光、強光、關燈 切換。

// OffLightState:
var OffLightState = function (light) {
  this.light = light;
};
OffLightState.prototype.buttonWasPressed = function () {
  console.log('弱光'); // offLightState 對應的行爲
  this.light.setState(this.light.weakLightState);  // 切換狀態到 weakLightState
};

// WeakLightState:
var WeakLightState = function (light) {
  this.light = light;
};
WeakLightState.prototype.buttonWasPressed = function () {
  console.log('強光'); // weakLightState 對應的行爲
  this.light.setState(this.light.strongLightState);  // 切換狀態到 strongLightState
};

// StrongLightState:
var StrongLightState = function (light) {
  this.light = light;
};
StrongLightState.prototype.buttonWasPressed = function () {
  console.log('關燈'); // strongLightState 對應的行爲
  this.light.setState(this.light.offLightState);  // 切換狀態到 offLightState
};

var Light = function () {
  this.offLightState = new OffLightState(this);
  this.weakLightState = new WeakLightState(this);
  this.strongLightState = new StrongLightState(this);
  this.button = null;
};

Light.prototype.init = function () {
  var button = document.createElement('button'),
    self = this;

  this.button = document.body.appendChild(button);
  this.button.innerHTML = '開關';

  this.currState = this.offLightState; // 設置當前狀態

  this.button.onclick = function () {
    self.currState.buttonWasPressed();
  }
};

Light.prototype.setState = function (newState) {
  this.currState = newState;
};

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

適配器模式

適配器模式的作用是解決兩個軟件實體間的接口不兼容的問題。

var googleMap = {
  show: function () {
    console.log('開始渲染谷歌地圖');
  }
};
var baiduMap = {
  display: function () {
    console.log('開始渲染百度地圖');
  }
};

// 添加百度地圖適配器
var baiduMapAdapter = {
  show: function () {
    return baiduMap.display();
  }
};

renderMap(googleMap);  // 輸出:開始渲染谷歌地圖
renderMap(baiduMapAdapter);  // 輸出:開始渲染百度地圖

總結

  • 單例模式:保證一個類僅有一個實例,並提供一個訪問它的全局訪問點。
  • 策略模式:封裝一系列目標一致的‘業務規則’。
  • 代理模式:當直接訪問本體不方便或者不符合需要時,爲這個本體提供一個替代者。
  • 迭代器模式:迭代器模式是指提供一種方法順序訪問一個聚合對象中的各個元素,而又不需要暴露該對象的內部表示。
  • 發佈-訂閱模式:定義對象間的一種一對多的依賴關係,當一個對象的狀態發生改變時,所有依賴於它的對象都將得到通知。
  • 命令模式:代碼封裝成命令,目的解藕。
  • 組合模式:組合對象-葉對象結構。
  • 模板方法模式:抽象父類,具體的實現子類。
  • 享元模式:一種用於性能優化的模式,核心是運用共享技術來有效支持大量細粒度的對象。
  • 職責鏈模式:使多個對象都有機會處理請求,當前不能解決則拋給下一個。
  • 中介者模式:解除對象與對象之間的緊耦合關係,使多對多變成了一對多。
  • 裝飾者模式:爲對象動態加入行爲。裝飾者模式經常會形成一條長長的裝飾鏈。
  • 狀態模式:狀態改變,行爲改變。
  • 適配器模式:解決兩個軟件實體間的接口不兼容的問題。

whosmeya.com

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章