【JavaScript】常用設計模式及程式設計技巧(ES6描述)

眼已望穿發表於2018-11-22

前言

平時的開發中可能不太需要用到設計模式,但是 JS 用上設計模式對於效能優化和專案工程化也是很有幫助的,下面就對常用的設計模式進行簡單的介紹與總結。

1. 單例模式

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

class Singleton {
  constructor(age) {
    this.age = age;
  }
  static getInstance(age) {
    const instance = Symbol.for('Singleton'); // 隱藏屬性,偽私有
    if (!Singleton[instance]) {
      Singleton[instance] = new Singleton(age);
    }
    return Singleton[instance];
  }
}

const singleton = Singleton.getInstance(30);
const singleton2 = Singleton.getInstance(20);
console.log(singleton === singleton2); // true
複製程式碼

2. 策略模式

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

策略模式的核心是整個分為兩個部分:

  • 第一部分是策略類,封裝具體的演算法;

  • 第二部分是環境類,負責接收客戶的請求並派發到策略類。

現在我們假定有這樣一個需求,需要對錶現為 S、A、B 的同事進行年終獎的計算,分別對應為 4 倍、3 倍、2 倍工資,常見的寫法如下:

const calculateBonus = (performanceLevel, salary) => {

  if (performanceLevel === 'S') {
    return salary * 4;
  }

  if (performanceLevel === 'A') {
    return salary * 3;
  }

  if (performanceLevel === 'B') {
    return salary * 2;
  }

};

calculateBonus('B', 20000); // 40000
複製程式碼

可以看到,程式碼裡面有較多的 if else 判斷語句,如果對應計算方式改變或者新增等級,我們都需要對函式內部進行調整,且薪資演算法重用性差,於是我們可以通過策略模式來進行重構,程式碼如下:

// 解決魔術字串
const strategyTypes = {
  S: Symbol('S'),
  A: Symbol('A'),
  B: Symbol('B'),
};

// 策略類
const strategies = {
  // S 級工資計算
  [strategyTypes.S](salary) {
    return salary * 4;
  },

  // A 工資計算
  [strategyTypes.A](salary) {
    return salary * 3;
  },

  // B 工資計算
  [strategyTypes.B](salary) {
    return salary * 2;
  }
  // 更多級別計算可以自由新增,且不會對原有部分造成影響
};

// 環境類
const calculateBonus = (level, salary) => { 
  return strategies[level](salary);
};

calculateBonus(strategyTypes.S, 300); // 1200
複製程式碼

策略模式的優點:

  • 利用組合、委託、多型等技術和思想,有效地避免了多重 if-else 語句;

  • 提供了對開放-封閉原則的完美支援,將演算法封裝在獨立的 strategy 中,使得它們易於切換、理解、擴充套件;

  • strategy 中的演算法也可以用在別處,避免許多複製貼上;

缺點:

  • 增加許多策略類或策略物件;

  • 違反知識最少原則;

3. 代理模式

定義:為一個物件提供一個代用品或佔位符,以便控制對它的訪問。

3.1 虛擬代理

在程式世界裡,操作可能是昂貴的,這時候 B 通過監聽 C 的狀態來將 A 的請求傳送過去(原本 A 需要實時去訪問 C準備請求),減少開銷。

代理的意義

單一職責: 就一個類(通常也包括物件和函式等)而言,應該僅有一個引起它變化的原因。如果一個物件承擔了多項職責,就意味著這個物件將變得巨大,引起它變化的原因可能會有多個。

例子:圖片載入前先顯示一張 loading 圖(預載入)。

// 立即執行函式建立 image,閉包設定 src
const myImage = (() => {
    const imgNode = document.createElement('img'); 
    document.body.appendChild(imgNode);
    return (src) => {
      imgNode.src = src;
    }
})();

// 代理 myImage,在 test.jpg onload 之前顯示 loading.gif
const proxyImage = (() => {
    const img = new Image();
    img.onload = () => {
      myImage(this.src);
    }
    return (src) => {
      myImage('./loading.gif');
      img.src = src;
    }
})();

proxyImage('./test.jpg');
複製程式碼

這裡的 myImage 只進行圖片 src 的設定,其他代理的工作交給了 proxyImage 方法,符合單一職責原則。此外,也保證了代理和本體介面的一致性。

3.2 快取代理

快取代理可以為一些開銷大的運算結果提供暫時的儲存,在下次運算時,如果傳遞進來的引數跟之前一致,則可以直接返回前面儲存的運算結果。

例子:計算乘積,快取 ajax 資料。

// 計算所有引數的乘積
const multi = (...args) => {
  let result = 1;
  args.forEach(arg => {
    result = result * arg;
  });
  return result;
};

// 快取計算結果函式
const proxyMulti = (() => {
  const cache = {}; // 快取池
  return (...args) => {
    const param = args.join(',');
    if (param in cache) {
      return cache[param]; // key 為 1,2,3,4,值為 24 的物件
    }
    cache[param] = multi.apply(this, args);
    return cache[param];
  };
})();

console.time();
console.log(proxyMulti(1, 2, 3, 4)); // 24
console.timeEnd(); // 約 0.7 ms

console.time();
console.log(proxyMulti(1, 2, 3, 4)); // 24
console.timeEnd(); // 約 0.1ms
複製程式碼

4. 觀察者模式

觀察者模式又叫釋出—訂閱模式,它定義物件間的一種一對多的依賴關係,當一個物件的狀態發生改變時,所有依賴於它的物件都將得到通知。在 JavaScript 開發中,我們一般用事件模型來替代傳統的觀察者模式。

4.1 DOM 事件

最早接觸到的觀察者模式大概就是 DOM 事件了,比如使用者的點選操作。我們沒辦法知道使用者什麼時候點選,但是當使用者點選時,被點選的節點就會向訂閱者釋出訊息。

document.body.addEventListener('click', () => {
  alert('我被點選啦!');
});
複製程式碼

4.2 自定義事件

要實現自定義事件,需要進行三步:

  1. 指定釋出者;
  2. 給釋出者新增一個快取列表,用以通知訂閱者;
  3. 遍歷快取列表依次觸發存放在裡面的訂閱者的回撥函式;
class Event {
  constructor() {
    this.eventListObj = {}; // 事件列表物件
  }

  // 單例
  static getInstance() {
    const instance = Symbol.for('instance');
    if (!Event[instance]) {
      Event[instance] = new Event();
    }
    return Event[instance];
  }

  // 新增監聽事件,同一指令,可以有多個事件
  listen(key, fn) {
    if (!this.eventListObj[key]) {
      this.eventListObj[key] = [];
    }
    // 訂閱訊息新增進快取列表
    this.eventListObj[key].push(fn);
  }

  // 觸發監聽事件
  trigger(key, ...args) {
    const fns = this.eventListObj[key];
    if (fns && fns.length !== 0) {
      fns.forEach(fn => {
        fn.apply(this, args);
      });
    }
  }

  // 移除監聽事件
  remove(key, fn) {
    let fns = this.eventListObj[key];
    // 被訂閱過才操作
    if (fns) {
      // 根據 fn 引數來判斷是全部移除還是指定移除
      if (!fn) {
        fns.length = 0; // 移除全部
      } else {
        // 移除某一個
        fn.forEach((f, index) => {
          if (f === fn) {
            fns.splice(index, 1);
          }
        });
      }
    }
  }
}

const event = Event.getInstance(); // 建立全域性釋出者

const add = (a, b) => {
  console.log(a + b);
};
const minus = (a, b) => {
  console.log(a - b);
};

event.listen('add', add); // 訂閱加法訊息
event.listen('minus', minus); // 訂閱減法訊息

event.trigger('add', 1, 3); // 觸發加法訂閱訊息
event.trigger('minus', 3, 1); // 觸發減法訂閱訊息

console.log(event); // Event 物件 eventListObj 屬性包含 add

event.remove('add', add); // 取消加法訂閱事件
console.log(event); // Event 物件 eventListObj 屬性不包含 add
複製程式碼

執行結果:

例子:ajax 請求登入後進行多種操作,以及在 vue 中 emit 和 on,node.js 中的 events

5. 模板方法模式

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

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

通常在抽象父類中封裝了子類的演算法框架,包括實現一些公共方法以及封裝子類中所有方法的執行順序。

子類通過繼承這個抽象類,也繼承了整個演算法結構,並且可以選擇重寫父類的方法。

下面我們來舉個例子——假如我們要泡一杯茶和一杯咖啡步驟如下:

  1. 把水煮沸
  2. 用沸水 ( 沖泡咖啡 / 浸泡茶葉 )
  3. 把 ( 咖啡 / 茶水 ) 倒進杯子
  4. 加糖和牛奶 / 加檸檬

很容易發現其中第一步是共有的,其他步驟大體一致,那麼我們就可以使用模板方法來實現它。( 假如有人不想加糖和牛奶怎麼辦呢? )

// 抽象出飲料類用來表示咖啡和茶
class Beverage {
  // 鉤子:解決了有人不想加糖和牛奶的問題
  customerWantsCondiments = true;
  init() {
    this.boilWater();
    this.brew();
    this.pourInCup();
    if (this.customerWantsCondiments) {
      this.addCondiments();
    }
  }
  // 第一步:把水煮沸
  boilWater(){
    console.log('把水煮沸');
  }
  // 第二步:沖泡飲料,在子類中重寫
  brew(){
    throw new Error('brew function must override in child');
  }
  // 第三步:倒出飲料,在子類中重寫
  pourInCup(){
    throw new Error('pourInCup function must override in child');
  }
  // 第四步:個性化飲料,在子類中重寫
  addCondiments(){
    throw new Error('addCondiments function must override in child');
  }
}

class Coffee extends Beverage {
  customerWantsCondiments = false;
  brew(){
    console.log('用沸水沖泡咖啡');
  }
  pourInCup(){
    console.log('把咖啡倒進杯子');
  }
  addCondiments(){
    console.log('加糖和牛奶');
  }
}

class Tea extends Beverage {
  brew(){
    console.log('用沸水浸泡茶葉');
  }
  pourInCup(){
    console.log('把茶水倒進杯子');
  }
  addCondiments(){
    console.log('加檸檬');
  }
}

new Coffee().init(); // 把水煮沸、用沸水沖泡咖啡、把咖啡倒進杯子
console.log('---------------');
new Tea().init(); // 把水煮沸、用沸水浸泡茶葉、把茶水倒進杯子、加檸檬
複製程式碼

6. 職責鏈模式

職責連模式:通過把物件連成一條鏈,讓請求沿著這條鏈傳遞,直到有一個物件能處理為止,解決了傳送者和接收者之間的耦合。

A --> B --> C --> ... --> N,中間有一個物件能處理 A 物件的請求,如果沒有需要在最後處理異常。

現實中的例子:早高峰擠公交的時候遞公交卡,只需要往前遞,總會遞到售票員手裡刷卡,而不用管遞給了誰,下面舉一個實際的例子來看看。

假如現在有個電商定金優惠券功能

  • 付 500 元定金可以獲得 100 元優惠券且一定能買到商品;
  • 付 200 元定金可以獲得 50 元優惠券且一定能買到商品;
  • 如果付定金只能進入普通購買,需要在庫存足夠的時候才可以買到商品;
  • 不付定金就是普通購買;

我們定義一個函式,接收三個引數:

  • orderType:1、2、3 分表代表 500 元定金, 200 元定金和無定金模式;
  • pay:true、false 代表拍下訂單是否付款;
  • stock:number 代表庫存餘量;
const order = (orderType, pay, stock) => {
  if (orderType === 1) {
    if (pay === true) {
      console.log('獲得 100 元優惠券');
    } else {
      if (stock > 0) {
        console.log('普通購買, 無優惠券');
      } else {
        console.log('庫存不足');
      }
    }
  } else if (orderType === 2) {
    if (pay === true) {
      console.log('獲得 50 元優惠券');
    } else {
      if (stock > 0) {
        console.log('普通購買, 無優惠券');
      } else {
        console.log('庫存不足');
      }
    }
  } else if (orderType === 3) {
    if (stock > 0) {
      console.log('普通購買, 無優惠券');
    } else {
      console.log('庫存不足');
    }
  }
}
order(1, true, 20); // 獲得 100 元優惠券
複製程式碼

這顯然不是一段好程式碼,大量的 if else 條件分支,如果業務再複雜一點,最後根本就沒法看了。

那麼我們通過 AOP(面向切面程式設計) 實現職責鏈:

const order500 = (orderType, pay, stock) => {
  // 如果支付了 500 元定金,就成功獲得 100 元優惠券
  if (orderType === 1 && pay === true) {
    return console.log('已支付定金,獲得100元優惠券');
  }
  // 否則進入下一步
  return 'NEXT';
}

const order200 = (orderType, pay, stock) => {
  // 如果支付了 200 元定金,就成功獲得 50 元優惠券
  if (orderType === 2 && pay === true) {
    return console.log('已支付定金,獲得50元優惠券');
  }
  // 否則進入下一步
  return 'NEXT';
}

// 普通購買模式
const orderNormal = (orderType, pay, stock) => {
  // 如果庫存大於 0,可以購買
  if (stock > 0) {
    return console.log('普通購買,無優惠券');
  }
  // 否則庫存不足,無法購買
  return console.log('庫存不足');

}

// 給 Funciton 掛載 after 方法,通過 NEXT 判斷是否進行下一步
Function.prototype.after = function(fn) {
  const self = this;
  return (...args) => {
    const result = self.apply(this, args);
    return result === 'NEXT' ? fn.apply(this, args) : result;
  }
}

const order = order500.after(order200).after(orderNormal); // 獲得 order 計算方式
order(1, false, 10); // 普通購買,無優惠券
複製程式碼

通過分解成三個獨立的函式,返回處理不了的結果'NEXT',交給下一個節點處理。通過 after 來進行繫結,最後我們在新增需求的時候可以在 after 中間插入即可,耦合度大大降低,但是這樣也有一個不好的地方,職責鏈過長增加了函式的作用域。

7. 中介者模式

在程式裡,物件經常會和其他物件進行通訊,當專案比較大,物件很多的時候,這種通訊就會形成一個通訊網,當我們想要修改某一個物件時,需要十分小心,以免這些改動牽一髮而動全身,導致出現BUG,非常的複雜。

中介者模式就是用來解除這些物件間的耦合,形成簡單的物件到中介者到物件的操作。

下面以現實中的機場指揮塔為例說明。

  • 如果沒有指揮塔的情況,每一架飛機都需要和其他飛機進行通訊,確保航線的安全,我們假設目的地相同就為航線不安全:
// 飛機類
class Plane {
  constructor(name, to) {
    this.name = name;
    this.to = to;
    this.otherPlanes = []; // 其他飛機集合
  }
  success() {
    console.log(`${this.name} 可以正常飛行`);
  }
  fail(plane) {
    console.log(`${this.name}${plane.name} 航線衝突,請調整`);
  }
  fly() {
    let normal = true; // 標誌位,是否可以正常飛行
    let targetPlane = {};
    for (let i = 0; i < this.otherPlanes.length; i++) {
      // 需要與其他每一架飛機比較,如果其他飛機與當前飛行航線衝突,則不可以飛行,
      if (this.otherPlanes[i].to === this.to) {
        normal = false;
        targetPlane = this.otherPlanes[i]; // 記住與當前飛機衝突的飛機
        break;
      }
    }
    if (normal) {
      this.success(); // 成功,可以飛行
      return;
    }
    this.fail(targetPlane); // 失敗,報告衝突飛機
  }
}

// 飛機工廠,建立飛機物件
class PlaneFactory {
  constructor() {
    this.planes = []; // 所有建立出來的飛機集合
  }
  static getInstance() {
    const instance = Symbol.for('instance');
    if (!PlaneFactory[instance]) {
      PlaneFactory[instance] = new PlaneFactory();
    }
    return PlaneFactory[instance];
  }
  // 建立飛機,飛機名稱和目的地
  createPlane(name, to) {
    const plane = new Plane(name, to);
    this.planes.push(plane);
    this.planes.forEach(planeItem => {
      // 如果飛機名稱與其他飛機名稱不一樣,將其他飛機放入 otherPlanes 陣列,用以目的地比較
      if (plane.name !== planeItem.name) {
        plane.otherPlanes.push(planeItem);
      }
    });
    // 返回當前飛機
    return plane;
  }
}

// 獲取飛機工廠例項
const planeFactory = PlaneFactory.getInstance();

// 建立四架飛機
const planeA = planeFactory.createPlane('planeA', 1);
const planeB = planeFactory.createPlane('planeB', 2);
const planeC = planeFactory.createPlane('planeC', 3);
const planeD = planeFactory.createPlane('planeD', 2);

planeA.fly(); // planeA 可以正常飛行
planeB.fly(); // planeB 可以正常飛行
planeC.fly(); // planeC 可以正常飛行
planeD.fly(); // planeD 與 planeB 航線衝突,請調整
複製程式碼

當飛機足夠多時,這樣的方式就會變得非常複雜,而且某一天有飛機出故障維修不參與飛行,那麼改動也是麻煩的。

  • 存在指揮塔的情況,飛機不需要知道其他飛機的存在,只需要向指揮塔通訊即可,而且新增了移除故障飛機的方法。
/**
  * 指揮塔模式:中介者
  * 指揮塔:接收飛機傳遞過來的一切資訊,同時操作需要操作的飛機
  * 飛機類:定義飛機物件,具備名稱和目的地,同時具備向指揮塔通訊的方法
  * 飛機工廠類:定義建立飛機物件的工廠,在建立時通知指揮塔新增飛機
  */
// 指揮塔
class Tower {
  constructor() {
    this.planes = []; // 飛機集合
    // 操作飛機物件
    this.operations = {
      add: this.add,
      remove: this.remove,
      fly: this.fly,
    };
  }
  static getInstance() {
    const instance = Symbol.for('instance'); // 防止被覆蓋
    if (!Tower[instance]) {
      Tower[instance] = new Tower();
    }
    return Tower[instance];
  }
  // 接收飛機傳遞給指揮塔的資訊
  receiveMessage(msg, ...args) {
    this.operations[msg].apply(this, args);
  }
  // 新增飛機
  add(plane) {
    this.planes.push(plane);
  }
  // 移除飛機
  remove(plane) {
    for (let i = 0; i < this.planes.length; i++) {
      if (this.planes[i].name === plane.name) {
        this.planes.splice(i, 1);
      }
    }
  }
  // 飛機開始飛行
  fly(plane) {
    let normal = true;
    let targetPlane = {};
    for (let i = 0; i < this.planes.length; i++) {
      // 如果當前飛機與所有飛機名稱不一致,但是航線一致,則認為航線衝突
      // 原本這個步驟放在了每個飛機裡,現在由指揮塔進行
      if (
        this.planes[i].name !== plane.name &&
        this.planes[i].to === plane.to
      ) {
        normal = false;
        targetPlane = this.planes[i];
        break;
      }
    }
    if (normal) {
      plane.success();
      return;
    }
    plane.fail(targetPlane);
  }
}

// 獲得指揮塔例項
const tower = Tower.getInstance();

// 飛機類:只需要設定名稱和向指揮塔通訊的方法就可以
class Plane {
  constructor(name, to) {
    this.name = name;
    this.to = to;
  }
  success() {
    console.log(`${this.name} 可以正常飛行`);
  }
  fail(plane) {
    console.log(`${this.name}${plane.name} 航線衝突,請調整`);
  }
  // 通知指揮塔該飛機移除
  remove() {
    tower.receiveMessage('remove', this);
  }
  // 通知指揮塔該飛機開始飛行
  fly() {
    tower.receiveMessage('fly', this);
  }
}

// 飛機工廠:建立飛機,同時向指揮塔通知 add 方法,新增飛機
class PlaneFactory {
  static plane(name, to) {
    const plane = new Plane(name, to);
    tower.receiveMessage('add', plane);
    return plane;
  }
}

// 建立四架飛機
const planeA = PlaneFactory.plane('planeA', 1);
const planeB = PlaneFactory.plane('planeB', 2);
const planeC = PlaneFactory.plane('planeC', 3);
const planeD = PlaneFactory.plane('planeD', 2);

planeA.fly(); // planeA 可以正常飛行
planeB.fly(); // planeB 與 planeD 航線衝突,請調整
planeC.fly(); // planeC 可以正常飛行
planeD.fly(); // planeD 與 planeB 航線衝突,請調整
planeD.remove(); // 假如 planeD 出故障了,進行移除
planeB.fly(); // planeB 可以正常飛行
複製程式碼

中介者模式是知識最少原則的一種實現,是指一個物件儘可能少的瞭解其他的物件,如果物件之間的耦合度過高,一個物件發生改變之後,難免會影響到其他物件,在中介者模式中,物件幾乎不知道其他物件的存在,它們只能通過中介者物件來通訊。但是這樣的結果就是中介者物件難免會變的臃腫。

8. 裝飾者模式

裝飾者(decorator)模式:給物件動態地增加職責的方式。

我們在開發中經常會使用到,因為在 JavaScript 中對物件動態操作是一件再簡單不過的事情了。

const person = {
  name: 'shelly',
  age: 18,
}
person.job = 'student';
複製程式碼

8.1 裝飾函式

給物件擴充套件屬性和方法相對簡單,但是在改寫函式時卻不是那麼容易,尤其是儘量保證開放-封閉原則的前提下。我們可以通過使用 AOP 裝飾函式來達到理想的效果。

let add = (a, b) => {
  console.log(a + b);
};
// 在函式執行之前執行
Function.prototype.before = function(beforeFn) {
  const self = this;
  return (...args) => {
    beforeFn.apply(this, args);
    return self.apply(this, args);
  };
};
// 在函式執行之後執行
Function.prototype.after = function(afterFn) {
  const self = this;
  return (...args) => {
    const result = self.apply(this, args);
    afterFn.apply(this, args);
    return result;
  };
};
// 裝飾 add 函式
add = add
  .before(() => {
    console.log('before add');
  })
  .after(() => {
    console.log('after add');
  });

add(1, 2); // before add、3、after add
複製程式碼

9. 設計原則和程式設計技巧

9.1 單一職責原則

單一職責原則(SRP):一個物件(方法)只做一件事情。如果一個方法承擔了過多的職責,將來改寫它的可能性就越大。

這一原則在單例模式、代理模式中都有廣泛的應用。

何時該分離

這是很難把控的一個點,比如 ajax 請求,建立 xhr 物件和傳送請求雖然是兩個職責,但是他們是一起變化,可以不用分離;像 jQuery 的 attr 方法,既賦值,又取值,理論上應該分離,卻方便了使用者。所以需要我們在實際上拿捏。

9.2 最少知識原則

最少知識原則(LKP):一個軟體實體應當儘可能地少於其他實體發生相互作用。這裡的實體包括了物件、類、模組、函式等。

常見的做法是引入第三方物件來承擔多個物件間的通訊,例如中介者模式、封裝。

9.3 開放 - 封閉原則

開放 - 封閉原則(OCP):軟體實體(類、模組、函式)等應該是可以擴充套件的,但是不可修改。

OCP 在幾乎所有的設計模式中得到了很好的表現。

9.3.1 擴充套件

假如我們要修改一個函式,業務邏輯極其複雜,那麼我們遵守開放 - 封閉原則在原來的基礎繫結一個 after 方法,傳入回撥函式實現我們新的需求而不用去改變之前的程式碼。

// 原來的函式
let theMostComplicatedFn = (a, b) => {
  console.log('我是極其複雜的函式');
  console.log(a + b);
};
// 定義在原來函式執行後執行的函式
theMostComplicatedFn.after = function(afterFn) { // 這裡由於 this 指向問題,需要用 fucntion
  const self = this;
  return (...args) => {
    const result = self.apply(this, args);
    afterFn.apply(this, args);
    return result;
  };
};
// 混合後的函式
theMostComplicatedFn = theMostComplicatedFn.after((a, b) => {
  console.log(a, b);
});

theMostComplicatedFn(1, 2); // 我是極其複雜的函式、3、1 2
複製程式碼

9.3.2 多型

利用物件的多型性也可以讓程式遵循開放 - 封閉原則,這是一個常用的技巧。

我們都知道貓吃魚,狗吃肉,那麼我們用程式碼來表達一下。

const food = (animal) => {
  if (animal instanceof Cat) {
    console.log('貓吃魚');
  } else if (animal instanceof Dog) {
    console.log('狗吃肉');
  }
}

class Dog {}
class Cat {}
food(new Dog()); // 狗吃肉
food(new Cat()); // 貓吃魚
複製程式碼

有一天加入了羊,又得再加一個 else if 來判斷,如果很多呢?那麼我們就要一直去改變 food 函式,這顯然不是一種好的方法。我們現在可以利用多型性,將共同的 food 抽取出來。

const food = (animal) => {
  animal.food();
}

class Dog {
  food() {
    console.log('狗吃肉');
  }
}
class Cat {
  food() {
    console.log('貓吃魚');
  }
}
class Sheep {
  food() {
    console.log('羊吃草');
  }
}
food(new Dog()); // 狗吃肉
food(new Cat()); // 貓吃魚
food(new Sheep()); // 羊吃草
複製程式碼

這樣,當我們以後要增加新的動物時,就不需要每次都去改變 food 函式了。

9.3.3 其他方式

鉤子函式、回撥函式。

9.4 程式碼重構

9.4.1 提煉函式

把一段程式碼提煉成函式的好處是:

  • 避免出現超大函式
  • 獨立出來的函式有利於程式碼複用
  • 獨立出來的函式更容易被覆寫
  • 獨立出來的函式如果有一個好的命名,它本身就起到了註釋的作用

9.4.2 合併重複的條件片段

如果一個函式體內有一些條件分支語句,而這些條件分支語句的內部散佈了一些重複的程式碼,那麼就有必要進行合併去重工作。

9.4.3 把條件分支語句提煉成函式

下面是一個例子:

const getPrice = (price) => {
  const date = new Date();
  if (date.getMonth() >= 6 && date.getMonth() <=9) { // 夏天
    return price * 0.8;
  }
  return price;
}
複製程式碼

條件語句乍一看需要理解一會兒,那麼此處可以做一下調整:

// 通過函式名也起到了註釋作用
const isSummer = () => {
  const month = new Date().getMonth();
  return month >= 6 && month <=9;
}

const getPrice = function (price) {
  const date = new Date();
  if (isSummer()) {
    return price * 0.8;
  }
  return price;
}
複製程式碼

9.4.4 合理使用迴圈

在函式體內,如果有些程式碼實際上負責的是一些重複性的工作,那麼合理利用迴圈不僅可以完成同樣的功能,還可以使程式碼量更少。我們以建立 xhr 物件為例:

const createXHR = () => {
  let xhr;
  try {
    xhr = new ActiveXObject('MSXML2.XMLHttp.6.0');
  } catch (e) {
    try {
      xhr = new ActiveXObject('MSXML2.XMLHttp.3.0');
    } catch (e) {
      xhr = new ActiveXObject('MSXML2.XMLHttp');
    }
  }
  return xhr;
};
const xhr = createXHR();
複製程式碼

下面我們通過迴圈,可以達到和上面一樣的效果:

const createXHR = function () {
  const versions = ['MSXML2.XMLHttp.6.0ddd', 'MSXML2.XMLHttp.3.0', 'MSXML2.XMLHttp'];
  for (let i = 0, version; version = versions[i++];) {
    try {
      return new ActiveXObject(version);
    } catch (e) {
    }
  }
};
const xhr = createXHR();
複製程式碼

9.4.5 提前讓函式退出代替巢狀條件分支

在多層條件分支語句中,我們可以挑選一些分支,在進入這些分支後,就立即讓函式退出,減少非關鍵程式碼的混淆。

9.4.6 傳遞物件引數代替過長的引數列表

函式引數過長過多會引起呼叫呼叫者的不適,可能出現傳少或傳反的情況。如果有這種情況,我們可以通過將引數包裝成一個物件傳入函式,然後在函式體內進行取值就可以了。

總結

在平時開發的過程中,策略模式、職責鏈模式還有裝飾者模式可能用的情況比較多,中介者和觀察者經常可以看到,雖然自己不一定親自設計,但是從原理上理解了他們也能方便我們更好地使用。

相關文章