JS中多種設(shè)計模式

在程序設(shè)計中有很多實用的設(shè)計模式,而其中大部分語言的實現(xiàn)都是基于“類”。
JavaScript中并沒有類這種概念,JS中的函數(shù)屬于一等對象,在JS定義一個對象非常簡單(var obj = {}),而基于JS中閉包與弱類型等特性,在實現(xiàn)一些設(shè)計模式的方式上與眾不同。

設(shè)計原則

單一職責(zé)原則(SRP)

一個對象或方法只做一件事情。如果一個方法承擔(dān)了過多的職責(zé),那么在需求的變遷過程中,需要改寫這個方法的可能性就越大。
應(yīng)該把對象或方法劃分成較小的粒度

最少知識原則(LKP)

一個軟件實體應(yīng)當(dāng) 盡可能少地與其他實體發(fā)生相互作用
應(yīng)當(dāng)盡量減少對象之間的交互。如果兩個對象之間不必彼此直接通信,那么這兩個對象就不要發(fā)生直接的 相互聯(lián)系,可以轉(zhuǎn)交給第三方進行處理

開放-封閉原則(OCP)

軟件實體(類、模塊、函數(shù))等應(yīng)該是可以 擴展的,但是不可修改
當(dāng)需要改變一個程序的功能或者給這個程序增加新功能的時候,可以使用增加代碼的方式,盡量避免改動程序的源代碼,防止影響原系統(tǒng)的穩(wěn)定

什么是設(shè)計模式

假設(shè)有一個空房間,我們要日復(fù)一日地往里 面放一些東西。最簡單的辦法當(dāng)然是把這些東西 直接扔進去,但是時間久了,就會發(fā)現(xiàn)很難從這 個房子里找到自己想要的東西,要調(diào)整某幾樣?xùn)| 西的位置也不容易。所以在房間里做一些柜子也 許是個更好的選擇,雖然柜子會增加我們的成 本,但它可以在維護階段為我們帶來好處。使用 這些柜子存放東西的規(guī)則,或許就是一種模式

  • 學(xué)習(xí)設(shè)計模式,有助于寫出可復(fù)用和可維護性高的程序
  • 設(shè)計模式的原則是“找出 程序中變化的地方,并將變化封裝起來”,它的關(guān)鍵是意圖,而不是結(jié)構(gòu)。
  • 不過要注意,使用不當(dāng)?shù)脑挘赡軙卤豆Π搿?/li>

一、單例模式

1. 定義

保證一個類僅有一個實例,并提供一個訪問它的全局訪問點

2. 核心

確保只有一個實例,并提供全局訪問

3. 實現(xiàn)

假設(shè)要設(shè)置一個管理員,多次調(diào)用也僅設(shè)置一次,我們可以使用閉包緩存一個內(nèi)部變量來實現(xiàn)這個單例

function SetManager(name) { 
  this.manager = name;
}

SetManager.prototype.getName = function() {
    console.log(this.manager);
}; 

var SingletonSetManager = (function() { 
  var manager = null; 
  return function(name) { 
    if (!manager) {
      manager = new SetManager(name);
    } 
    return manager;
  } 
})();

SingletonSetManager('a').getName(); // a
SingletonSetManager('b').getName(); // a
SingletonSetManager('c').getName(); // a

這是比較簡單的做法,但是假如我們還要設(shè)置一個HR呢?就得復(fù)制一遍代碼了
所以,可以改寫單例內(nèi)部,實現(xiàn)地更通用一些

// 提取出通用的單例
function getSingleton(fn) { 
  var instance = null; 
  return function() { 
    if (!instance) {
      instance = fn.apply(this, arguments);
    } 
    return instance;
  }
}

再進行調(diào)用,結(jié)果還是一樣

// 獲取單例
var managerSingleton = getSingleton(function(name) { 
  var manager = new SetManager(name); 
  return manager;
});

managerSingleton('a').getName(); // a
managerSingleton('b').getName(); // a
managerSingleton('c').getName(); // a

這時,我們添加HR時,就不需要更改獲取單例內(nèi)部的實現(xiàn)了,僅需要實現(xiàn)添加HR所需要做的,再調(diào)用即可

function SetHr(name) { 
  this.hr = name;
}

SetHr.prototype.getName = function() {
  console.log(this.hr);
}; 
var hrSingleton = getSingleton(function(name) { 
  var hr = new SetHr(name); 
  return hr;
});

hrSingleton('aa').getName(); // aa
hrSingleton('bb').getName(); // aa
hrSingleton('cc').getName(); // aa

或者,僅想要創(chuàng)建一個div層,不需要將對象實例化,直接調(diào)用函數(shù)
結(jié)果為頁面中僅有第一個創(chuàng)建的div

function createPopup(html) { 
  var div = document.createElement('div');
  div.innerHTML = html;
  document.body.append(div); 
  return div;
} 

var popupSingleton = getSingleton(function() { 
  var div = createPopup.apply(this, arguments); 
  return div;
});

console.log(
    popupSingleton('aaa').innerHTML,
    popupSingleton('bbb').innerHTML,
    popupSingleton('bbb').innerHTML
); // aaa  aaa  aaa

二、策略模式

1. 定義

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

2. 核心

將算法的使用和算法的實現(xiàn)分離開來。

一個基于策略模式的程序至少由兩部分組成:

  • 第一個部分是一組策略類,策略類封裝了具體的算法,并負(fù)責(zé)具體的計算過程。
  • 第二個部分是環(huán)境類Context,Context接受客戶的請求,隨后把請求委托給某一個策略類。要做到這點,說明Context 中要維持對某個策略對象的引用

3. 實現(xiàn)

策略模式可以用于組合一系列算法,也可用于組合一系列業(yè)務(wù)規(guī)則
假設(shè)需要通過成績等級來計算學(xué)生的最終得分,每個成績等級有對應(yīng)的加權(quán)值。我們可以利用對象字面量的形式直接定義這個組策略

// 加權(quán)映射關(guān)系
var levelMap = {
  S: 10,
  A: 8,
  B: 6,
  C: 4 
}; 
// 組策略
var scoreLevel = {
  basicScore: 80,
  S: function() { 
    return this.basicScore + levelMap['S']; 
  },
  A: function() { 
    return this.basicScore + levelMap['A']; 
  },
  B: function() { 
    return this.basicScore + levelMap['B']; 
  },
  C: function() { 
    return this.basicScore + levelMap['C']; 
  }
} 
// 調(diào)用
function getScore(level) { 
  return scoreLevel[level] ? scoreLevel[level]() : 0;
}

console.log(
    getScore('S'),
    getScore('A'),
    getScore('B'),
    getScore('C'),
    getScore('D')
); // 90 88 86 84 0

在組合業(yè)務(wù)規(guī)則方面,比較經(jīng)典的是表單的驗證方法。這里列出比較關(guān)鍵的部分

// 錯誤提示
var errorMsgs = { 
  default: '輸入數(shù)據(jù)格式不正確',
  minLength: '輸入數(shù)據(jù)長度不足',
  isNumber: '請輸入數(shù)字',
  required: '內(nèi)容不為空' 
}; 
// 規(guī)則集
var rules = {
  minLength: function(value, length, errorMsg) { 
    if (value.length < length) { 
      return errorMsg || errorMsgs['minLength']
    }
  },
  isNumber: function(value, errorMsg) { 
    if (!/\d+/.test(value)) { 
      return errorMsg || errorMsgs['isNumber'];
    }
  },
  required: function(value, errorMsg) { 
    if (value === '') { 
      return errorMsg || errorMsgs['required'];
    }
  }
}; 
// 校驗器
function Validator() { 
  this.items = [];
};

Validator.prototype = {
  constructor: Validator, // 添加校驗規(guī)則
  add: function(value, rule, errorMsg) { 
    var arg = [value]; 
    if (rule.indexOf('minLength') !== -1) { 
      var temp = rule.split(':');
      arg.push(temp[1]);
      rule = temp[0];
    }
    arg.push(errorMsg); 
    this.items.push(function() { 
      // 進行校驗
      return rules[rule].apply(this, arg);
    });
  }, 
  // 開始校驗
  start: function() { 
    for (var i = 0; i < this.items.length; ++i) { 
      var ret = this.items[i](); 
      if (ret) {
        console.log(ret); 
        // return ret;
      }
    }
  }
}; 
// 測試數(shù)據(jù)
function testTel(val) { 
  return val;
} 
var validate = new Validator();

validate.add(testTel('ccc'), 'isNumber', '只能為數(shù)字'); // 只能為數(shù)字
validate.add(testTel(''), 'required'); // 內(nèi)容不為空
validate.add(testTel('123'), 'minLength:5', '最少5位'); // 最少5位
validate.add(testTel('12345'), 'minLength:5', '最少5位'); 
var ret = validate.start();

console.log(ret);

4. 優(yōu)缺點

優(yōu)點

可以有效地避免多重條件語句,將一系列方法封裝起來也更直觀,利于維護

缺點

往往策略集會比較多,我們需要事先就了解定義好所有的情況

三、代理模式

1. 定義

為一個對象提供一個代用品或占位符,以便控制對它的訪問

2. 核心

當(dāng)客戶不方便直接訪問一個 對象或者不滿足需要的時候,提供一個替身對象 來控制對這個對象的訪問,客戶實際上訪問的是 替身對象。

替身對象對請求做出一些處理之后, 再把請求轉(zhuǎn)交給本體對象

代理和本體的接口具有一致性,本體定義了關(guān)鍵功能,而代理是提供或拒絕對它的訪問,或者在訪問本體之前做一 些額外的事情

3. 實現(xiàn)

代理模式主要有三種:保護代理、虛擬代理、緩存代理
保護代理主要實現(xiàn)了訪問主體的限制行為,以過濾字符作為簡單的例子

// 主體,發(fā)送消息
function sendMsg(msg) {
  console.log(msg);
} 
// 代理,對消息進行過濾
function proxySendMsg(msg) { 
  // 無消息則直接返回
  if (typeof msg === 'undefined') {
    console.log('deny'); return;
  } 
  // 有消息則進行過濾
  msg = ('' + msg).replace(/泥\s*煤/g, '');
  sendMsg(msg);
}

sendMsg('泥煤呀泥 煤呀'); // 泥煤呀泥 煤呀
proxySendMsg('泥煤呀泥 煤'); // 呀
proxySendMsg(); // deny
  • 它的意圖很明顯,在訪問主體之前進行控制,沒有消息的時候直接在代理中返回了,拒絕訪問主體,這數(shù)據(jù)保護代理的形式
  • 有消息的時候?qū)γ舾凶址M行了處理,這屬于虛擬代理的模式
  • 虛擬代理在控制對主體的訪問時,加入了一些額外的操作
  • 在滾動事件觸發(fā)的時候,也許不需要頻繁觸發(fā),我們可以引入函數(shù)節(jié)流,這是一種虛擬代理的實現(xiàn)
// 函數(shù)防抖,頻繁操作中不處理,直到操作完成之后(再過 delay 的時間)才一次性處理
function debounce(fn, delay) {
  delay = delay || 200; 
  var timer = null; 
  return function() { 
    var arg = arguments; 
    // 每次操作時,清除上次的定時器
    clearTimeout(timer);
    timer = null; 
    // 定義新的定時器,一段時間后進行操作
    timer = setTimeout(function() {
      fn.apply(this, arg);
    }, delay);
  }
}; 
var count = 0; // 主體
function scrollHandle(e) {
  console.log(e.type, ++count); // scroll
} 
// 代理
var proxyScrollHandle = (function() { 
  return debounce(scrollHandle, 500);
})();

window.onscroll = proxyScrollHandle;

緩存代理可以為一些開銷大的運算結(jié)果提供暫時的緩存,提升效率

來個栗子,緩存加法操作

// 主體
function add() { var arg = [].slice.call(arguments); return arg.reduce(function(a, b) { return a + b;
    });
} // 代理
var proxyAdd = (function() { var cache = []; return function() { var arg = [].slice.call(arguments).join(','); // 如果有,則直接從緩存返回
        if (cache[arg]) { return cache[arg];
        } else { var ret = add.apply(this, arguments); return ret;
        }
    };
})();

console.log(
    add(1, 2, 3, 4),
    add(1, 2, 3, 4),

    proxyAdd(10, 20, 30, 40),
    proxyAdd(10, 20, 30, 40)
); // 10 10 100 100

四、迭代器模式

1. 定義

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

2. 核心

在使用迭代器模式之后,即使不關(guān)心對象的內(nèi)部構(gòu)造,也可以按順序訪問其中的每個元素

3. 實現(xiàn)

JS中數(shù)組的map forEach 已經(jīng)內(nèi)置了迭代器

[1, 2, 3].forEach(function(item, index, arr) {
  console.log(item, index, arr);
});

不過對于對象的遍歷,往往不能與數(shù)組一樣使用同一的遍歷代碼

我們可以封裝一下

function each(obj, cb) { 
  var value; if (Array.isArray(obj)) { 
    for (var i = 0; i < obj.length; ++i) {
      value = cb.call(obj[i], i, obj[i]); 
      if (value === false) { 
        break;
      }
    }
  } else { 
    for (var i in obj) {
      value = cb.call(obj[i], i, obj[i]); 
      if (value === false) { 
        break;
      }
    }
  }
}

each([1, 2, 3], function(index, value) {
  console.log(index, value);
});

each({a: 1, b: 2}, function(index, value) {
  console.log(index, value);
}); 
// 0 1 
// 1 2 
// 2 3

// a 1 
// b 2

再來看一個例子,強行地使用迭代器,來了解一下迭代器也可以替換頻繁的條件語句

雖然例子不太好,但在其他負(fù)責(zé)的分支判斷情況下,也是值得考慮的

function getManager() { 
  var year = new Date().getFullYear(); 
  if (year <= 2000) {
    console.log('A');
  } else if (year >= 2100) {
    console.log('C');
  } else {
    console.log('B');
  }
}

getManager(); // B

將每個條件語句拆分出邏輯函數(shù),放入迭代器中迭代

function year2000() { 
  var year = new Date().getFullYear(); 
  if (year <= 2000) {
    console.log('A');
  } 
  return false;
} 
function year2100() { 
  var year = new Date().getFullYear(); 
  if (year >= 2100) {
    console.log('C');
  } 
  return false;
} 
function year() { 
  var year = new Date().getFullYear(); 
  if (year > 2000 && year < 2100) {
    console.log('B');
  } 
  return false;
} 
function iteratorYear() { 
  for (var i = 0; i < arguments.length; ++i) { 
    var ret = arguments[i](); 
    if (ret !== false) { 
      return ret;
    }
  }
} 
var manager = iteratorYear(year2000, year2100, year); // B

五、發(fā)布-訂閱模式

1. 定義

也稱作觀察者模式,定義了對象間的一種一對多的依賴關(guān)系,當(dāng)一個對象的狀態(tài)發(fā) 生改變時,所有依賴于它的對象都將得到通知

2. 核心

取代對象之間硬編碼的通知機制,一個對象不用再顯式地調(diào)用另外一個對象的某個接口。
與傳統(tǒng)的發(fā)布-訂閱模式實現(xiàn)方式(將訂閱者自身當(dāng)成引用傳入發(fā)布者)不同,在JS中通常使用注冊回調(diào)函數(shù)的形式來訂閱

3. 實現(xiàn)

JS中的事件就是經(jīng)典的發(fā)布-訂閱模式的實現(xiàn)

// 訂閱
document.body.addEventListener('click', function() {
  console.log('click1');
}, false);

document.body.addEventListener('click', function() {
  console.log('click2');
}, false); 
// 發(fā)布
document.body.click(); // click1  click2

自己實現(xiàn)一下

小A在公司C完成了筆試及面試,小B也在公司C完成了筆試。他們焦急地等待結(jié)果,每隔半天就電話詢問公司C,導(dǎo)致公司C很不耐煩。
一種解決辦法是 AB直接把聯(lián)系方式留給C,有結(jié)果的話C自然會通知AB

// 觀察者
var observer = { 
  // 訂閱集合
  subscribes: [], 
  // 訂閱
  subscribe: function(type, fn) { 
    if (!this.subscribes[type]) { 
      this.subscribes[type] = [];
    } 
    // 收集訂閱者的處理
    typeof fn === 'function' && this.subscribes[type].push(fn);
  }, 
  // 發(fā)布  可能會攜帶一些信息發(fā)布出去
  publish: function() { 
    var type = [].shift.call(arguments),
    fns = this.subscribes[type]; 
    // 不存在的訂閱類型,以及訂閱時未傳入處理回調(diào)的
    if (!fns || !fns.length) { 
      return;
    } 
    // 挨個處理調(diào)用
    for (var i = 0; i < fns.length; ++i) {
      fns[i].apply(this, arguments);
    }
  }, 
  // 刪除訂閱
  remove: function(type, fn) { 
    // 刪除全部
    if (typeof type === 'undefined') { 
      this.subscribes = []; return;
    } 
    var fns = this.subscribes[type]; 
    // 不存在的訂閱類型,以及訂閱時未傳入處理回調(diào)的
    if (!fns || !fns.length) { 
      return;
    } 
    if (typeof fn === 'undefined') {
      fns.length = 0; return;
    } // 挨個處理刪除
    for (var i = 0; i < fns.length; ++i) { 
      if (fns[i] === fn) {
        fns.splice(i, 1);
      }
    }
  }
}; 
// 訂閱崗位列表
function jobListForA(jobs) {
  console.log('A', jobs);
} 
function jobListForB(jobs) {
  console.log('B', jobs);
} 
// A訂閱了筆試成績
observer.subscribe('job', jobListForA); 
// B訂閱了筆試成績
observer.subscribe('job', jobListForB); 

// A訂閱了筆試成績
observer.subscribe('examinationA', function(score) {
  console.log(score);
}); 
// B訂閱了筆試成績
observer.subscribe('examinationB', function(score) {
  console.log(score);
}); 

// A訂閱了面試結(jié)果
observer.subscribe('interviewA', function(result) {
  console.log(result);
});

observer.publish('examinationA', 100); // 100
observer.publish('examinationB', 80); // 80
observer.publish('interviewA', '備用'); // 備用
 observer.publish('job', ['前端', '后端', '測試']); // 輸出A和B的崗位

// B取消訂閱了筆試成績
observer.remove('examinationB'); // A都取消訂閱了崗位
observer.remove('job', jobListForA);

observer.publish('examinationB', 80); // 沒有可匹配的訂閱,無輸出
observer.publish('job', ['前端', '后端', '測試']); // 輸出B的崗位

4. 優(yōu)缺點

優(yōu)點

一為時間上的解耦,二為對象之間的解耦。可以用在異步編程中與MV框架中

缺點

創(chuàng)建訂閱者本身要消耗一定的時間和內(nèi)存,訂閱的處理函數(shù)不一定會被執(zhí)行,駐留內(nèi)存有性能開銷
弱化了對象之間的聯(lián)系,復(fù)雜的情況下可能會導(dǎo)致程序難以跟蹤維護和理解

六、命令模式

1. 定義

用一種松耦合的方式來設(shè)計程序,使得請求發(fā)送者和請求接收者能夠消除彼此之間的耦合關(guān)系
命令(command)指的是一個執(zhí)行某些特定事情的指令

2. 核心

命令中帶有execute執(zhí)行、undo撤銷、redo重做等相關(guān)命令方法,建議顯示地指示這些方法名

3. 實現(xiàn)

簡單的命令模式實現(xiàn)可以直接使用對象字面量的形式定義一個命令

var incrementCommand = {
  execute: function() { 
    // something
  }
};

不過接下來的例子是一個自增命令,提供執(zhí)行、撤銷、重做功能
采用對象創(chuàng)建處理的方式,定義這個自增

// 自增
function IncrementCommand() { 
  // 當(dāng)前值
  this.val = 0; 
  // 命令棧
  this.stack = [];
  // 棧指針位置
  this.stackPosition = -1;
};

IncrementCommand.prototype = {
  constructor: IncrementCommand, 
  // 執(zhí)行
  execute: function() { 
    this._clearRedo(); 
    // 定義執(zhí)行的處理
    var command = function() { 
      this.val += 2;
    }.bind(this); 
    // 執(zhí)行并緩存起來
    command(); 
    this.stack.push(command); 
    this.stackPosition++; 
    this.getValue();
  },
  canUndo: function() { 
    return this.stackPosition >= 0;
  },
  canRedo: function() { 
    return this.stackPosition < this.stack.length - 1;
  }, 
  // 撤銷
  undo: function() { 
    if (!this.canUndo()) { 
      return;
    } 
    this.stackPosition--; 
    // 命令的撤銷,與執(zhí)行的處理相反
    var command = function() { 
      this.val -= 2;
    }.bind(this); 
    // 撤銷后不需要緩存
    command(); 
    this.getValue();
  }, 
  // 重做
  redo: function() { 
    if (!this.canRedo()) { 
      return;
    } 
    // 執(zhí)行棧頂?shù)拿?    this.stack[++this.stackPosition](); 
    this.getValue();
  }, 
  // 在執(zhí)行時,已經(jīng)撤銷的部分不能再重做
  _clearRedo: function() { 
    this.stack = this.stack.slice(0, this.stackPosition + 1);
  }, 
  // 獲取當(dāng)前值
  getValue: function() {
    console.log(this.val);
  }
};

再實例化進行測試,模擬執(zhí)行、撤銷、重做的操作

var incrementCommand = new IncrementCommand(); 
// 模擬事件觸發(fā),執(zhí)行命令
var eventTrigger = { 
  // 某個事件的處理中,直接調(diào)用命令的處理方法
  increment: function() {
    incrementCommand.execute();
  },
  incrementUndo: function() {
    incrementCommand.undo();
  },
  incrementRedo: function() {
    incrementCommand.redo();
  }
};

eventTrigger['increment'](); // 2
eventTrigger['increment'](); // 4
eventTrigger['incrementUndo'](); // 2
eventTrigger['increment'](); // 4
eventTrigger['incrementUndo'](); // 2
eventTrigger['incrementUndo'](); // 0
eventTrigger['incrementUndo'](); // 無輸出
eventTrigger['incrementRedo'](); // 2
eventTrigger['incrementRedo'](); // 4
eventTrigger['incrementRedo'](); // 無輸出
eventTrigger['increment'](); // 6

此外,還可以實現(xiàn)簡單的宏命令(一系列命令的集合)

var MacroCommand = {
  commands: [],
  add: function(command) { 
    this.commands.push(command); 
    return this;
  },
  remove: function(command) { 
    if (!command) { 
      this.commands = []; 
      return;
    } 
    for (var i = 0; i < this.commands.length; ++i) { 
      if (this.commands[i] === command) { 
        this.commands.splice(i, 1);
      }
    }
  },
  execute: function() { 
    for (var i = 0; i < this.commands.length; ++i) { 
      this.commands[i].execute();
    }
  }
}; 
var showTime = {
  execute: function() {
    console.log('time');
  }
}; 
var showName = {
  execute: function() {
    console.log('name');
  }
}; 
var showAge = {
  execute: function() {
    console.log('age');
  }
};

MacroCommand.add(showTime).add(showName).add(showAge);

MacroCommand.remove(showName);

MacroCommand.execute(); // time age

七、組合模式

1. 定義

是用小的子對象來構(gòu)建更大的 對象,而這些小的子對象本身也許是由更小 的“孫對象”構(gòu)成的。

2. 核心

可以用樹形結(jié)構(gòu)來表示這種“部分- 整體”的層次結(jié)構(gòu)。
調(diào)用組合對象 的execute方法,程序會遞歸調(diào)用組合對象 下面的葉對象的execute方法

但要注意的是,組合模式不是父子關(guān)系,它是一種HAS-A(聚合)的關(guān)系,將請求委托給 它所包含的所有葉對象?;谶@種委托,就需要保證組合對象和葉對象擁有相同的 接口

此外,也要保證用一致的方式對待 列表中的每個葉對象,即葉對象屬于同一類,不需要過多特殊的額外操作

3. 實現(xiàn)

使用組合模式來實現(xiàn)掃描文件夾中的文件

// 文件夾 組合對象
function Folder(name) { 
  this.name = name; 
  this.parent = null; this.files = [];
}

Folder.prototype = {
  constructor: Folder,
  add: function(file) {
    file.parent = this; this.files.push(file); 
    return this;
  },
  scan: function() { 
    // 委托給葉對象處理
    for (var i = 0; i < this.files.length; ++i) { 
      this.files[i].scan();
    }
  },
  remove: function(file) { 
    if (typeof file === 'undefined') { 
      this.files = []; return;
    } 
    for (var i = 0; i < this.files.length; ++i) { 
      if (this.files[i] === file) { 
        this.files.splice(i, 1);
      }
    }
  }
}; 
// 文件 葉對象
function File(name) { 
  this.name = name; 
  this.parent = null;
}

File.prototype = {
  constructor: File,
  add: function() {
    console.log('文件里面不能添加文件');
  },

  scan: function() { 
    var name = [this.name]; var parent = this.parent; while (parent) {
      name.unshift(parent.name);
      parent = parent.parent;
    }
    console.log(name.join(' / '));
  }
};

構(gòu)造好組合對象與葉對象的關(guān)系后,實例化,在組合對象中插入組合或葉對象

var web = new Folder('Web'); 
var fe = new Folder('前端'); 
var css = new Folder('CSS'); 
var js = new Folder('js'); 
var rd = new Folder('后端');

web.add(fe).add(rd); 
var file1 = new File('HTML權(quán)威指南.pdf'); 
var file2 = new File('CSS權(quán)威指南.pdf'); 
var file3 = new File('JavaScript權(quán)威指南.pdf'); 
var file4 = new File('MySQL基礎(chǔ).pdf'); 
var file5 = new File('Web安全.pdf'); 
var file6 = new File('Linux菜鳥.pdf');

css.add(file2);
fe.add(file1).add(file3).add(css).add(js);
rd.add(file4).add(file5);
web.add(file6);

rd.remove(file4); 
// 掃描
web.scan();

掃描結(jié)果為

4. 優(yōu)缺點

優(yōu)點

可以方便地構(gòu)造一棵樹來表示對象的部分-整體 結(jié)構(gòu)。在樹的構(gòu)造最終 完成之后,只需要通過請求樹的最頂層對 象,便能對整棵樹做統(tǒng)一一致的操作。

缺點

創(chuàng)建出來的對象長得都差不多,可能會使代碼不好理解,創(chuàng)建太多的對象對性能也會有一些影響

八、模板方法模式

1. 定義

模板方法模式由兩部分結(jié)構(gòu)組成,第一部分是抽象父類,第二部分是具體的實現(xiàn)子類。

2. 核心

在抽象父類中封裝子類的算法框架,它的 init方法可作為一個算法的模板,指導(dǎo)子類以何種順序去執(zhí)行哪些方法。
由父類分離出公共部分,要求子類重寫某些父類的(易變化的)抽象方法

3. 實現(xiàn)

模板方法模式一般的實現(xiàn)方式為繼承
以運動作為例子,運動有比較通用的一些處理,這部分可以抽離開來,在父類中實現(xiàn)。具體某項運動的特殊性則有自類來重寫實現(xiàn)。
最終子類直接調(diào)用父類的模板函數(shù)來執(zhí)行

// 體育運動
function Sport() {

}

Sport.prototype = {
  constructor: Sport, 
  // 模板,按順序執(zhí)行
  init: function() { 
    this.stretch(); 
    this.jog(); 
    this.deepBreath();   
    this.start(); 
    var free = this.end(); 

    // 運動后還有空的話,就拉伸一下
    if (free !== false) { 
      this.stretch();
    }
  }, 

  // 拉伸
  stretch: function() {
    console.log('拉伸');
  }, 

  // 慢跑
  jog: function() {
    console.log('慢跑');
  }, 

  // 深呼吸
  deepBreath: function() {
    console.log('深呼吸');
  }, 

  // 開始運動
  start: function() { 
    throw new Error('子類必須重寫此方法');
  }, 

  // 結(jié)束運動
  end: function() {
    console.log('運動結(jié)束');
  }
}; 

// 籃球
function Basketball() {

}

Basketball.prototype = new Sport(); // 重寫相關(guān)的方法
Basketball.prototype.start = function() {
    console.log('先投上幾個三分');
};

Basketball.prototype.end = function() {
    console.log('運動結(jié)束了,有事先走一步'); return false;
}; // 馬拉松
function Marathon() {

}

Marathon.prototype = new Sport(); 

var basketball = new Basketball(); 
var marathon = new Marathon(); 
// 子類調(diào)用,最終會按照父類定義的順序執(zhí)行
basketball.init();
marathon.init();

九、享元模式

1. 定義

享元(flyweight)模式是一種用于性能優(yōu)化的模式,它的目標(biāo)是盡量減少共享對象的數(shù)量

2. 核心

運用共享技術(shù)來有效支持大量細(xì)粒度的對象。

強調(diào)將對象的屬性劃分為內(nèi)部狀態(tài)(屬性)與外部狀態(tài)(屬性)。內(nèi)部狀態(tài)用于對象的共享,通常不變;而外部狀態(tài)則剝離開來,由具體的場景決定。

3. 實現(xiàn)

在程序中使用了大量的相似對象時,可以利用享元模式來優(yōu)化,減少對象的數(shù)量

舉個栗子,要對某個班進行身體素質(zhì)測量,僅測量身高體重來評判

// 健康測量
function Fitness(name, sex, age, height, weight) { 
  this.name = name; this.sex = sex; 
  this.age = age; 
  this.height = height; 
  this.weight = weight;
} 
// 開始評判
Fitness.prototype.judge = function() { 
  var ret = this.name + ': '; 
  if (this.sex === 'male') {
    ret += this.judgeMale();
  } else {
    ret += this.judgeFemale();
  }
  console.log(ret);
}; 
// 男性評判規(guī)則
Fitness.prototype.judgeMale = function() { 
  var ratio = this.height / this.weight; 
  return this.age > 20 ? (ratio > 3.5) : (ratio > 2.8);
};
// 女性評判規(guī)則
Fitness.prototype.judgeFemale = function() { 
  var ratio = this.height / this.weight; 
  return this.age > 20 ? (ratio > 4) : (ratio > 3);
}; 
var a = new Fitness('A', 'male', 18, 160, 80); 
var b = new Fitness('B', 'male', 21, 180, 70); 
var c = new Fitness('C', 'female', 28, 160, 80); 
var d = new Fitness('D', 'male', 18, 170, 60); 
var e = new Fitness('E', 'female', 18, 160, 40); 
// 開始評判
a.judge(); // A: false
b.judge(); // B: false
c.judge(); // C: false
d.judge(); // D: true
e.judge(); // E: true

評判五個人就需要創(chuàng)建五個對象,一個班就幾十個對象

可以將對象的公共部分(內(nèi)部狀態(tài))抽離出來,與外部狀態(tài)獨立。將性別看做內(nèi)部狀態(tài)即可,其他屬性都屬于外部狀態(tài)。

這么一來我們只需要維護男和女兩個對象(使用factory對象),而其他變化的部分則在外部維護(使用manager對象)

// 健康測量
function Fitness(sex) { 
  this.sex = sex;
} 
// 工廠,創(chuàng)建可共享的對象
var FitnessFactory = {
  objs: [],
  create: function(sex) { 
    if (!this.objs[sex]) { 
      this.objs[sex] = new Fitness(sex);
    } 
    return this.objs[sex];
  }
}; 
// 管理器,管理非共享的部分
var FitnessManager = {
  fitnessData: {}, 
  // 添加一項
  add: function(name, sex, age, height, weight) { 
    var fitness = FitnessFactory.create(sex); 
    // 存儲變化的數(shù)據(jù)
    this.fitnessData[name] = {
      age: age,
      height: height,
      weight: weight
    }; 
    return fitness;
  }, 
  // 從存儲的數(shù)據(jù)中獲取,更新至當(dāng)前正在使用的對象
  updateFitnessData: function(name, obj) { 
    var fitnessData = this.fitnessData[name]; 
    for (var item in fitnessData) { 
      if (fitnessData.hasOwnProperty(item)) {
        obj[item] = fitnessData[item];
      }
    }
  }
}; 

// 開始評判
Fitness.prototype.judge = function(name) { 
  // 操作前先更新當(dāng)前狀態(tài)(從外部狀態(tài)管理器中獲?。?  FitnessManager.updateFitnessData(name, this); 
  var ret = name + ': '; 
  if (this.sex === 'male') {
    ret += this.judgeMale();
  } else {
    ret += this.judgeFemale();
  }
  console.log(ret);
}; 

// 男性評判規(guī)則
Fitness.prototype.judgeMale = function() { 
  var ratio = this.height / this.weight; 
  return this.age > 20 ? (ratio > 3.5) : (ratio > 2.8);
}; 

// 女性評判規(guī)則
Fitness.prototype.judgeFemale = function() { 
  var ratio = this.height / this.weight; 
  return this.age > 20 ? (ratio > 4) : (ratio > 3);
}; 

var a = FitnessManager.add('A', 'male', 18, 160, 80); 
var b = FitnessManager.add('B', 'male', 21, 180, 70); 
var c = FitnessManager.add('C', 'female', 28, 160, 80); 
var d = FitnessManager.add('D', 'male', 18, 170, 60); 
var e = FitnessManager.add('E', 'female', 18, 160, 40); 

// 開始評判
a.judge('A'); // A: false
b.judge('B'); // B: false
c.judge('C'); // C: false
d.judge('D'); // D: true
e.judge('E'); // E: true

不過代碼可能更復(fù)雜了,這個例子可能還不夠充分,只是展示了享元模式如何實現(xiàn),它節(jié)省了多個相似的對象,但多了一些操作。

factory對象有點像單例模式,只是多了一個sex的參數(shù),如果沒有內(nèi)部狀態(tài),則沒有參數(shù)的factory對象就更接近單例模式了

十、職責(zé)鏈模式

1. 定義

使多個對象都有機會處理請求,從而避免請求的發(fā)送者和接收者之間的耦合關(guān)系,將這些對象連成一條鏈,并沿著這條鏈 傳遞該請求,直到有一個對象處理它為止

2. 核心

請求發(fā)送者只需要知道鏈中的第一個節(jié)點,弱化發(fā)送者和一組接收者之間的強聯(lián)系,可以便捷地在職責(zé)鏈中增加或刪除一個節(jié)點,同樣地,指定誰是第一個節(jié)點也很便捷

3. 實現(xiàn)

以展示不同類型的變量為例,設(shè)置一條職責(zé)鏈,可以免去多重if條件分支

// 定義鏈的某一項
function ChainItem(fn) { 
  this.fn = fn; this.next = null;
}

ChainItem.prototype = {
  constructor: ChainItem, 
  // 設(shè)置下一項
  setNext: function(next) { 
    this.next = next; return next;
  }, 
  // 開始執(zhí)行
  start: function() { 
    this.fn.apply(this, arguments);
  }, 
  // 轉(zhuǎn)到鏈的下一項執(zhí)行
  toNext: function() { 
    if (this.next) { 
      this.start.apply(this.next, arguments);
    } else {
      console.log('無匹配的執(zhí)行項目');
    }
  }
}; 

// 展示數(shù)字
function showNumber(num) { 
  if (typeof num === 'number') {
    console.log('number', num);
  } else { 
    // 轉(zhuǎn)移到下一項
    this.toNext(num);
  }
} 

// 展示字符串
function showString(str) { 
  if (typeof str === 'string') {
    console.log('string', str);
  } else { 
    this.toNext(str);
  }
} 

// 展示對象
function showObject(obj) { 
  if (typeof obj === 'object') {
    console.log('object', obj);
  } else { 
    this.toNext(obj);
  }
} 

var chainNumber = new ChainItem(showNumber); 
var chainString = new ChainItem(showString); 
var chainObject = new ChainItem(showObject); 

// 設(shè)置鏈條
chainObject.setNext(chainNumber).setNext(chainString);

chainString.start('12'); // string 12
chainNumber.start({}); // 無匹配的執(zhí)行項目
chainObject.start({}); // object {}
chainObject.start(123); // number 123

這時想判斷未定義的時候呢,直接加到鏈中即可

// 展示未定義
function showUndefined(obj) { 
  if (typeof obj === 'undefined') {
    console.log('undefined');
  } else { 
    this.toNext(obj);
  }
} 

var chainUndefined = new ChainItem(showUndefined);
chainString.setNext(chainUndefined);

chainNumber.start(); // undefined

由例子可以看到,使用了職責(zé)鏈后,由原本的條件分支換成了很多對象,雖然結(jié)構(gòu)更加清晰了,但在一定程度上可能會影響到性能,所以要注意避免過長的職責(zé)鏈。

十一、中介者模式

1. 定義

所有的相關(guān) 對象都通過中介者對象來通信,而不是互相引用,所以當(dāng)一個對象發(fā)生改變時,只需要通知中介者對象即可

2. 核心

使網(wǎng)狀的多對多關(guān)系變成了相對簡單的一對多關(guān)系(復(fù)雜的調(diào)度處理都交給中介者)

使用中介者后

3. 實現(xiàn)

多個對象,指的不一定得是實例化的對象,也可以將其理解成互為獨立的多個項。當(dāng)這些項在處理時,需要知曉并通過其他項的數(shù)據(jù)來處理。

如果每個項都直接處理,程序會非常復(fù)雜,修改某個地方就得在多個項內(nèi)部修改

我們將這個處理過程抽離出來,封裝成中介者來處理,各項需要處理時,通知中介者即可。

var A = {
  score: 10,
  changeTo: function(score) { 
    this.score = score; 
    // 自己獲取
    this.getRank();
  }, 
  // 直接獲取
  getRank: function() { 
    var scores = [this.score, B.score, C.score].sort(function(a, b) { 
      return a < b;
    });
    console.log(scores.indexOf(this.score) + 1);
  }
}; 

var B = {
  score: 20,
  changeTo: function(score) { 
    this.score = score; 
    // 通過中介者獲取
    rankMediator(B);
  }
}; 

var C = {
  score: 30,
  changeTo: function(score) { 
    this.score = score;
    rankMediator(C);
  }
}; 

// 中介者,計算排名
function rankMediator(person) { 
  var scores = [A.score, B.score, C.score].sort(function(a, b) { 
    return a < b;
  });
  console.log(scores.indexOf(person.score) + 1);
} 

// A通過自身來處理
A.changeTo(100); // 1

// B和C交由中介者處理
B.changeTo(200); // 1
C.changeTo(50); // 3

ABC三個人分?jǐn)?shù)改變后想要知道自己的排名,在A中自己處理,而B和C使用了中介者。B和C將更為輕松,整體代碼也更簡潔

最后,雖然中介者做到了對模塊和對象的解耦,但有時對象之間的關(guān)系并非一定要解耦,強行使用中介者來整合,可能會使代碼更為繁瑣,需要注意。

十二、裝飾者模式

1. 定義

以動態(tài)地給某個對象添加一些額外的職責(zé),而不會影響從這個類中派生的其他對象。
是一種“即用即付”的方式,能夠在不改變對 象自身的基礎(chǔ)上,在程序運行期間給對象動態(tài)地 添加職責(zé)

2. 核心

是為對象動態(tài)加入行為,經(jīng)過多重包裝,可以形成一條裝飾鏈

3. 實現(xiàn)

最簡單的裝飾者,就是重寫對象的屬性

var A = {
  score: 10 
};

A.score = '分?jǐn)?shù):' + A.score;

可以使用傳統(tǒng)面向?qū)ο蟮姆椒▉韺崿F(xiàn)裝飾,添加技能

function Person() {}

Person.prototype.skill = function() {
  console.log('數(shù)學(xué)');
}; 

// 裝飾器,還會音樂
function MusicDecorator(person) { 
  this.person = person;
}

MusicDecorator.prototype.skill = function() { 
  this.person.skill();
  console.log('音樂');
}; 

// 裝飾器,還會跑步
function RunDecorator(person) { 
  this.person = person;
}

RunDecorator.prototype.skill = function() { 
  this.person.skill();
  console.log('跑步');
}; 

var person = new Person(); 

// 裝飾一下
var person1 = new MusicDecorator(person);
person1 = new RunDecorator(person1);

person.skill(); // 數(shù)學(xué)
person1.skill(); // 數(shù)學(xué) 音樂 跑步

在JS中,函數(shù)為一等對象,所以我們也可以使用更通用的裝飾函數(shù)

// 裝飾器,在當(dāng)前函數(shù)執(zhí)行前先執(zhí)行另一個函數(shù)
function decoratorBefore(fn, beforeFn) { 
  return function() { 
    var ret = beforeFn.apply(this, arguments); 
    // 在前一個函數(shù)中判斷,不需要執(zhí)行當(dāng)前函數(shù)
    if (ret !== false) {
      fn.apply(this, arguments);
    }
  };
} 

function skill() {
  console.log('數(shù)學(xué)');
} 
function skillMusic() {
  console.log('音樂');
} 
function skillRun() {
  console.log('跑步');
} 

var skillDecorator = decoratorBefore(skill, skillMusic);
skillDecorator = decoratorBefore(skillDecorator, skillRun);

skillDecorator(); // 跑步 音樂 數(shù)學(xué)

十三、狀態(tài)模式

1. 定義

事物內(nèi)部狀態(tài)的改變往往會帶來事物的行為改變。在處理的時候,將這個處理委托給當(dāng)前的狀態(tài)對象即可,該狀態(tài)對象會負(fù)責(zé)渲染它自身的行為

2. 核心

區(qū)分事物內(nèi)部的狀態(tài),把事物的每種狀態(tài)都封裝成單獨的類,跟此種狀態(tài)有關(guān)的行為都被封裝在這個類的內(nèi)部

3. 實現(xiàn)

以一個人的工作狀態(tài)作為例子,在剛醒、精神、疲倦幾個狀態(tài)中切換著

// 工作狀態(tài)
function Work(name) { 
  this.name = name; 
  this.currentState = null; 
  // 工作狀態(tài),保存為對應(yīng)狀態(tài)對象
  this.wakeUpState = new WakeUpState(this); 
  // 精神飽滿
  this.energeticState = new EnergeticState(this); 
  // 疲倦
  this.tiredState = new TiredState(this); 
  this.init();
}

Work.prototype.init = function() { 
  this.currentState = this.wakeUpState; 
  // 點擊事件,用于觸發(fā)更新狀態(tài)
  document.body.onclick = () => { 
    this.currentState.behaviour();
  };
}; 

// 更新工作狀態(tài)
Work.prototype.setState = function(state) { 
  this.currentState = state;
} 

// 剛醒
function WakeUpState(work) { 
  this.work = work;
} 

// 剛醒的行為
WakeUpState.prototype.behaviour = function() {
  console.log(this.work.name, ':', '剛醒呢,睡個懶覺先'); 
  // 只睡了2秒鐘懶覺就精神了..
  setTimeout(() => { 
    this.work.setState(this.work.energeticState);
  }, 2 * 1000);
} 

// 精神飽滿
function EnergeticState(work) { 
  this.work = work;
}

EnergeticState.prototype.behaviour = function() {
  console.log(this.work.name, ':', '超級精神的'); 
  // 才精神1秒鐘就發(fā)困了
  setTimeout(() => { 
    this.work.setState(this.work.tiredState);
  }, 1000);
}; 

// 疲倦
function TiredState(work) { 
  this.work = work;
}

TiredState.prototype.behaviour = function() {
  console.log(this.work.name, ':', '怎么肥事,好困'); 
  // 不知不覺,又變成了剛醒著的狀態(tài)... 不斷循環(huán)呀
  setTimeout(() => { 
    this.work.setState(this.work.wakeUpState);
  }, 1000);
}; 

var work = new Work('曹操');

點擊一下頁面,觸發(fā)更新狀態(tài)的操作

4. 優(yōu)缺點

優(yōu)點

狀態(tài)切換的邏輯分布在狀態(tài)類中,易于維護

缺點

多個狀態(tài)類,對于性能來說,也是一個缺點,這個缺點可以使用享元模式來做進一步優(yōu)化
將邏輯分散在狀態(tài)類中,可能不會很輕易就能看出狀態(tài)機的變化邏輯

十四、適配器模式

1. 定義

是解決兩個軟件實體間的接口不兼容的問題,對不兼容的部分進行適配

2. 核心

解決兩個已有接口之間不匹配的問題

3. 實現(xiàn)

比如一個簡單的數(shù)據(jù)格式轉(zhuǎn)換的適配器

// 渲染數(shù)據(jù),格式限制為數(shù)組了
function renderData(data) {
  data.forEach(function(item) {
    console.log(item);
  });
} 

// 對非數(shù)組的進行轉(zhuǎn)換適配
function arrayAdapter(data) { 
  if (typeof data !== 'object') { 
    return [];
  } 
  if (Object.prototype.toString.call(data) === '[object Array]') { 
    return data;
  } 
  var temp = []; 
  for (var item in data) { 
    if (data.hasOwnProperty(item)) {
      temp.push(data[item]);
    }
  } 
  return temp;
} 
var data = { 
  0: 'A', 
  1: 'B', 
  2: 'C' 
};

renderData(arrayAdapter(data)); // A B C

十五、外觀模式

1. 定義

為子系統(tǒng)中的一組接口提供一個一致的界面,定義一個高層接口,這個接口使子系統(tǒng)更加容易使用

2. 核心

可以通過請求外觀接口來達到訪問子系統(tǒng),也可以選擇越過外觀來直接訪問子系統(tǒng)

3. 實現(xiàn)

外觀模式在JS中,可以認(rèn)為是一組函數(shù)的集合

// 三個處理函數(shù)
function start() {
  console.log('start');
} 

function doing() {
  console.log('doing');
} 

function end() {
  console.log('end');
} 

// 外觀函數(shù),將一些處理統(tǒng)一起來,方便調(diào)用
function execute() {
  start();
  doing();
  end();
} 

// 調(diào)用init開始執(zhí)行
function init() { 
  // 此處直接調(diào)用了高層函數(shù),也可以選擇越過它直接調(diào)用相關(guān)的函數(shù)
  execute();
}

init(); // start doing end
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容