使人拍案叫絕的四種設計模式!

0. 回顧過去

說實話,這標題有點兒 uc小編 的味道了。雖然真正的大佬已經對設計模式爛熟於胸,只但願個人學習記錄能幫助到部分同窗就足夠了。通過我下面的介紹,你能夠在極短的時間,瞭解並知道如何使用他們。html

通過一兩個月的分享斷斷續續的,我一共分享了 11 種 Javascript 設計模式,其中:前端

每一種設計模式都是前輩們總結多年的經驗,實屬精華。儘管我不能徹底出神入化的運用它,但在一個程序但設計上,必定會或多或少的來借鑑他們的思想。自從學習了設計模式,在寫代碼的時候,終於不會氣喘吁吁,一口氣寫五個組件,不費勁。vue

那下面,接着介紹四種,使人拍案叫絕的!設計模式。web

1. 原型模式

簡介

原型模式(prototype)是指用原型實例指向建立對象的種類,而且經過拷貝這些原型建立新的對象。vuex

說實話,每次一看設計模式的簡介就頭大,明明每一個字都認識,結合起來就懵了。和我同樣狀況的小夥伴,建議直接看代碼,有的時候...看看註釋,看看設計模式的名字,也許就豁然開朗!編程

實際操做

原型模式的使用可讓咱們獲得,原始對象附帶的一些屬性,以下:redux

var lol = {
 server: '比爾吉沃特',  startGame: function () {  console.log('link start!')  } };  // 經過原型模式,新建同一個服務器的新用戶 var User = Object.create(lol, {  'name': {  value: '黃梵高'  } });  複製代碼

上面能夠說就是一個完整的原型模式。把原有對象內包含的屬性,經過Object.create函數,成功拷貝。而且在建立新對象時,添加進入新增的name字段,十分人性化。後端

若是說你以爲,哦,我這要兼容 ie8 的,用不來Object.create這種高級瀏覽器才支持的函數。那徹底能夠的,下面再來介紹不使用這函數的原型模式實現:設計模式

var lol = {
 server: '比爾吉沃特',  startGame: function () {  console.log('link start!')  },  init: function (name) { // 須要增長一個接口,用於修改內部屬性  this.name = name  }, };   function User(name) {  function F() { };  F.prototype = lol;   var f = new F();  f.init(name);  return f; }  var user = User('黃梵高'); user.startGame(); // user.server 複製代碼

這種原型模式的實現方式,和上面的Object.create方式略有不一樣,由於建立了一個名叫F的構造函數,而且提早暴漏了接口init才得以修改內部屬性,和直接建立對象相比天然是冗餘了很多。數組

不過相信兼容 ie8 的需求也仍是存在的,不少時候仍是不得不使用它啊!

那恭喜你學會了原型模式,由於十分簡單,平時開發也會不經意間用到,但要注意淺拷貝和深拷貝的問題哦。先來道開胃菜,這波啊這波是一道肉蛋蔥雞。

2. 觀察者模式

介紹

觀察者模式又叫發佈訂閱模式(Publish/Subscribe),它定義了一種一對多的關係,讓多個觀察者對象同時監聽某一個主題對象,這個主題對象的狀態發生變化時就會通知全部的觀察者對象,使得它們可以自動更新本身。

實踐環節

其實觀察者模式,真的太熟悉了。就是當你開源項目終於發佈了,你但願不少人都知道,但不能你一個一個去告訴吧,那也太卑微了。因此最好的方式是你有一羣粉絲,他們翹首以盼着等着你的開源項目,終於你在b站,開了一個發佈會。不少粉絲爭先恐後地去聽,聽完以後回家根據這個新框架開始練手。這就構成了一個觀察者模式。

觀察者的使用場合就是:當一個對象的改變須要同時改變其它對象,而且它不知道具體有多少對象須要改變的時候,就應該考慮使用觀察者模式。

先來一個十分簡陋的模式例子:

var timer = setInterval(() => {  if (window.good) {  clearInterval(timer)  console.log('good了')  } }, 500)  setTimeout(() => {  window.good = 1 }, 2000)  複製代碼
  • s: 實現成功!教練我學會了!這邊瘋狂修改全局變量,另外一邊瘋狂event loop就行了,500毫秒延遲過高了,我再低一點,調個50吧,再來一個 for 循環,遍歷建立事件就能夠了!

  • t: 且不說性能問題,那若是頁面不少,你在某一個頁面拋出的變量必定能接的到嗎?或者會不會有變量衝突,多人維護的時候怎麼能保證全局變量的統一處理?

  • s: 那教練我不會了。

  • t: 下面教練教你一招,經過存放回調函數的方式隊列執行:

var pubsub = {};
(function (q) {  var topics = {}, // 回調函數存放的數組,把全部的  subUid = -1;  // 發佈方法  q.publish = function (topic, args) {  if (!topics[topic]) {  return false;  }  setTimeout(function () {  var subscribers = topics[topic], // 名字爲topic變量的事件隊列  len = subscribers ? subscribers.length : 0;  while (len--) {  subscribers[len].func(topic, args); // 循環執行傳進來的函數  }  }, 0); // 使用setTimeout保證先執行完同步代碼邏輯  return true;  };  //訂閱方法  q.subscribe = function (topic, func) {  if (!topics[topic]) {  topics[topic] = [];  }  var token = (++subUid).toString();  topics[topic].push({  token: token,  func: func  });  return token;  };  //退訂方法  q.unsubscribe = function (token) {  for (var m in topics) { // 先找到要退訂的事件隊列  if (topics[m]) {  for (var i = 0, j = topics[m].length; i < j; i++) { // 傳進來的token,是在觸發訂閱函數時生成的token  if (topics[m][i].token === token) { // 找到專屬token,進行隊列刪除操做  topics[m].splice(i, 1);  return token;  }  }  }  }  return false;  }; } (pubsub));  //將訂閱賦值給一個變量,以便退訂 var sub = pubsub.subscribe('lol', function (topics, data) {  console.log(topics + ": " + data); });  //發佈通知 pubsub.publish('lol', 'hello world!'); pubsub.publish('lol', ['test', 'a', 'b', 'c']); pubsub.publish('lol', [{ 'color': 'blue' }, { 'text': 'hello'}]);  setTimeout(function () {  pubsub.unsubscribe(sub); }, 0);  pubsub.publish('lol', 'hello world!'); // 不會再執行訂閱時傳入的函數了  複製代碼

以上就實現了一個,以事件回調爲基礎的觀察者模式模型。擁有訂閱,發佈,退訂操做。能夠知足大部分對於一個對象的改變須要同時改變其它對象,而且它不知道具體有多少對象須要改變的狀況。

經過事件隊列依次執行,若是須要加強函數功能,只須要擴展函數便可。

另一個觀察者顯而易見的例子:若是你平時使用 vue React 等框架的話,裏面的 redux vuex 就涉及到觀察者模式。

小結

可是,觀察者雖好,可不要貪杯嗷。爲何這麼說呢?

  1. 一個觀察者模式的實現,不免須要很長的邏輯,不免影響一些內存
  2. 若是在被觀察者之間有循環依賴的話,被觀察者會觸發它們之間進行循環調用,致使系統崩潰。在使用觀察者模式是要特別注意這一點。
  3. 觀察者模式可使觀察者知道所觀察的對象發生了變化,但觀察者模式沒有使觀察者知道所觀察的對象是怎麼發生變化的。
  4. 觀察者模式讓一個本能夠不觸發的事件,一直停留在內存中,即便事件不會執行。
  5. 弱化了對象之間的聯繫,會致使項目的難以跟蹤維護和理解。

只要不濫用,觀察者模式仍是可以讓邏輯分離,實現代碼解耦,提升可維護程度。總比用setInterval來循環監聽全局變量要好得多吧。

3. 迭代器模式

簡介

迭代器模式(Iterator):提供一種方法順序一個聚合對象中各個元素,而又不暴露該對象內部表示。

其實就是在不暴露對象的狀況,能夠拿到全部對象內的 元素。

其實ES6設計了Iterator的概念

下面是阮大官網介紹的栗子

實際操做

var it = makeIterator(['a', 'b']);
 it.next() // { value: "a", done: false } it.next() // { value: "b", done: false } it.next() // { value: undefined, done: true }  function makeIterator(array) {  var nextIndex = 0;  return {  next: function() {  return nextIndex < array.length ?  {value: array[nextIndex++], done: false} :  {value: undefined, done: true};  }  }; }  複製代碼

簡單看一下代碼,很清晰,十分易於理解,迭代器模式要的就是不須要關心數據結構,但只要咱們調用it.next()方法,就能夠知道對象全部的內容,按照順序返回給咱們!

而咱們並不會暴露it對象裏面全部的數據,十分人性化。

可是須要注意的是,在JavaScript的世界裏,並不是全部對象都有迭代器,類數組,數組,字符串是擁有迭代器的,但一個普通的對象必須部署了 Iterator 接口後才能使用。

那咱們再舉個栗子🌰:

var obj = {  name: 123,  age: 18,  info: [{  friend: 'abc',  title: 'ba'  }] }  //  複製代碼

這樣的一個對象,若是想能拿到全部對象,但不把對象暴露出來該怎麼辦呢?

這種狀況對於前端開發能夠說司空見慣了,後端給的數據必定是多層嵌套的,只要是 JavaScript 有的數據類型,確定都有機會出現,那若是但願遍歷取值怎麼辦?

相信Object.keys()是不少人的選擇!

for (var key of Object.keys(obj)) {
 console.log(key + ': ' + obj[key]); }  //name: 123 //age: 18 //info: [object Object] 複製代碼

小結

上面就實現了迭代器模式,不少人都認爲迭代器模式比較簡單,甚至不少語言都會內置迭代器,方便使用,更有甚者認爲這不屬於一種設計模式。

其實設計模式也就是前人總結的設計經驗,建築學中有着更多的設計模式,軟件工程學中有着相比之下較少,但精華的模式,正由於它十分優秀,纔會在多種編程語言下大放異彩,而不該由於它被內置而再也不說起。

4. 中介者模式

中介者模式,一看名字就知道,應該是有一個東西做爲兩端溝通的中介。好比,咱們平時租房買房,不免要和中介打交道,不少時候就被中介把買賣雙方都坑了,這種時候就是個十分差勁的中介者模式實踐😠!

簡介

中介者模式(Mediator),用一箇中介對象來封裝一系列的對象交互。中介者使各對象不須要顯式地相互引用,從而使其耦合鬆散,並且能夠獨立地改變它們之間的交互。

中介者模式的本質就是,有一個集權的管理控制函數,可以作到單向的接受信息並進行分發消息和動做。

與觀察者模式的區別

這兩種模式真的很像,因而網絡上很容易的找到一段不明因此的話。

觀察者模式,沒有封裝約束的單個對象,相反,觀察者Observer和具體類Subject是一塊兒配合來維護約束的,溝通是經過多個觀察者和多個具體類來交互的:每一個具體類一般包含多個觀察者,而有時候具體類裏的一個觀察者也是另外一個觀察者的具體類。而中介者模式所作的不是簡單的分發,倒是扮演着維護這些約束的職責。

這句話能夠說,十分晦澀難懂...

用點清楚的白話來描述兩者區別:

觀察者模式,確定會有一個觀察者的列表,其中可能會有增長,刪除,插入,置空等等接口函數。調用觀察者函數的,能夠經過這些接口,進行一些操做,也就是和原有的觀察者函數,共同來維護觀察者列表!

中介者模式特色就是:不須要調用中介者函數的,來進行一些對中介者列表對處理。由於這份列表,是中介者函數提供的,並不須要共同維護,只須要中介者函數本身來維護。而後和觀察者模式一樣的,擁有分發和監聽對功能!

差異也就是,具體的監聽列表,能不能用調用它對函數進行維護。

實踐環節

function LOL (username) {
 this.username = username  this.task = {} } LOL.prototype.on = function (type, callback) {  this.task[type].push(callback) } LOL.prototype.emit = function (type) {  this.task[type].forEach(item => {  item && item()  }) }  var mine = new LOL('黃梵高') mine.on('start', function(){  console.log('start') }) mine.emit('start') 複製代碼

上面的手寫代碼,能夠說觀察者模式和中介者模式,均可以這麼實現。具體區別就是觀察者模式的話,實現還應該會多出unemit, empty等等函數,便於操做觀察者列表。

中介者模式內部應該還會有一些其餘處理,好比:

function LOL (username) {
 this.username = username  this.task = {} } LOL.prototype.on = function (type, callback) {  this.task[type].push(callback) } LOL.prototype.emit = function (type) {  if (type === 'stop') { // 若是事件爲中止,則把start列表所有清空  this.task['start'] = []  }  this.task[type].forEach(item => {  item && item()  }) } 複製代碼

如上註釋處,中介者模式會把維護列表的工做,與本身融爲一體,省着你在外面操做。

中介者模式也一樣能夠用,買賣租房的中介來理解。不管中介是好是坑,其實做爲找中介的咱們,也沒辦法去改變它給咱們提供的房屋列表。

小結

中介者模式並不困難,某種程度上和觀察者實現差很少。固然兩者也有區別,剛纔也已經敘述。請根據業務需求具體使用。

總結

設計模式能夠幫助咱們設計函數結構,易於維護,開發也能夠避免失誤。但過分設計也會形成資源浪費,開發週期增長等缺點,因此必定要適度結合使用。在頻繁改動的項目,即便你設計的十分優雅,也有可能直接被產品把功能砍掉...不管怎樣抽象解耦,也必定要適度而行。好比我目前維護的項目,頻繁使用觀察者模式並不適合,會形成不少的資源浪費,某些狀況下,甚至調整 dom 資源加載順序也能夠解決一些問題(開發時可能會有不少種不一樣方案,請用性價比最高的方案!)。

參考文獻

Tom大叔博客

本文使用 mdnice 排版

相關文章
相關標籤/搜索