使用JavaScript學習設計模式

前言

去年的時候先是看了修言大佬的性能優化掘金小冊子,收穫良多。javascript

以後緊接着買了這本JavaScript 設計模式核⼼原理與應⽤實踐,恰好最近有小冊免費學的活動,就趕忙把這篇筆記整理出來了,而且補充了小冊子中的沒有寫到的其他設計模式,學習過程當中結合 JavaScript 編寫的例子,以便於理解和加深印象。html

與其說是一篇文章,其實更像是一篇總結性質的學習筆記。前端

爲何要學習設計模式?

學習以前,先了解什麼是設計模式?java

設計模式(Design Pattern)是前輩們對代碼開發經驗的總結,是解決特定問題的一系列套路。它不是語法規定,而是一套用來提升代碼可複用性、可維護性、可讀性、穩健性以及安全性的解決方案。

簡答理解 它是一套被反覆使用、多人知曉的、通過分類的、代碼設計經驗總結。node

烹飪有菜譜,遊戲有攻略,每一個領域都存在一些可以讓咱們又好又快地達成目標的「套路」。在程序世界,編程的「套路」就是設計模式。webpack

學習它也就是學習這個編程世界的套路,對之後升級打怪打裝備有很大的幫助。在瞬息萬變的前端領域,設計模式也是一種「一次學習,終生受用」知識。ios

設計模式的原則

描述一個不斷髮生的重複的問題,以及該問題的解決方案的核心。
這樣,你就能一次又一次的使用該方案而沒必要作重複勞動。

一大法則:git

  • 迪米特法則:又叫最少知識法則,一個軟件實體應該儘量少的語其餘實體發生相互做用,每個軟件單位對其餘的單位都只有最少的知識,並且侷限於那些與本單位密切相關的軟件單位。

五大原則:es6

  • 單一職責原則:一個類,應該僅有一個引發它變化的緣由,簡而言之,就是功能要單一。
  • 開放封閉原則:對擴展開放,對修改關閉。
  • 里氏替換原則:基類出現的地方,子類必定出現。
  • 接口隔離原則:一個藉口應該是一種角色,不應乾的事情不敢,該乾的都要幹。簡而言之就是下降耦合、減低依賴。
  • 依賴翻轉原則:針對接口編程,依賴抽象而不依賴具體。

JavaScript 中經常使用的是單一功能和開放封閉原則。github

高內聚和低耦合

經過設計模式能夠幫助咱們加強代碼的可重用性、可擴充性、 可維護性、靈活性好。咱們使用設計模式最終的目的是實現代碼的 高內聚 和 低耦合。

舉例一個現實生活中的例子,例如一個公司,通常都是各個部門各司其職,互不干涉。各個部門須要溝通時經過專門的負責人進行對接。

在軟件裏面也是同樣的 一個功能模塊只是關注一個功能,一個模塊最好只實現一個功能,這個是所謂的內聚

模塊與模塊之間、系統與系統之間的交互,是不可避免的, 可是咱們要儘可能減小因爲交互引發的單個模塊沒法獨立使用或者沒法移植的狀況發生, 儘量多的單獨提供接口用於對外操做, 這個就是所謂的低耦合

封裝變化

在實際開發過程當中,不發生變化的代碼基本是不存在的,因此我要將代碼的變化最小化。

設計模式的核心就是去觀察你整個邏輯裏的變與不變,而後將不變分離,達到使變化的部分靈活、不變的地方穩定的目的。

設計模式的種類

經常使用的能夠分爲建立型、結構型、行爲型三類,一共 23 種模式。

建立型:

結構型:

行爲型:

建立型

工廠模式

這種類型的設計模式屬於建立型模式,它提供了一種建立對象的最佳方式。

在工廠模式中,咱們在建立對象時不會對客戶端暴露建立邏輯,而且是經過使用一個共同的接口來指向新建立的對象。
在 JS 中其實就是藉助構造函數實現。

例子

某個班級要作一個錄入系統,錄入一我的,就要寫一次。

let liMing = {
  name: "李明",
  age: 20,
  sex: "男",
};

若是多個錄入,則能夠建立一個類。

class Student {
  constructor(name, age, sex) {
    this.name = name;
    this.age = age;
    this.sex = sex;
  }
}
let zhangSan = new Student("張三", 19, "男");

工廠模式是將建立對象的過程單獨封裝,使用使只須要無腦傳參就好了,就像一個工廠同樣,只要給夠原料,就能夠輕易的製造出成品。

小結

  • 構造函數和建立者分離,對 new 操做進行封裝
  • 符合開放封閉原則

單例模式

單例模式的定義:保證一個類僅有一個實例,而且提供一個訪問它的全局變量。

實現的方法爲前判斷實例是否存在,若是存在直接返回,不存在則建立在返回,這就確保了一個類只有一個實例對象。

好比:Vuex、jQuery

例子

使用場景:一個單一對象,好比:彈窗,不管點擊多少次,彈窗只應被建立一次,實現起來也很簡單,用一個變量緩存就好了。

【點擊查看Demo】:單例模式-在線例子

如上面這個彈框,只有在第一次點擊按鈕時纔會建立彈框,以後都不會在建立,而是使用以前建立的彈框。

如此,即是實現了一個應用於單例模式的彈框。

小結

  • 維持一個實例,若是已經建立,就直接返回
  • 符合開放封閉原則

原型模式

用原型實例指定建立對象的種類,而且經過拷貝這些原型建立新的對象。

例子

在 JavaScript 中,實現原型模式是在 ECMAscript5 中,提出的 Object.create 方法,使用現有的對象來提供建立的對象__proto__

var prototype = {
  name: "Jack",
  getName: function() {
    return this.name;
  },
};

var obj = Object.create(prototype, {
  job: {
    value: "IT",
  },
});

console.log(obj.getName()); // Jack
console.log(obj.job); // IT
console.log(obj.__proto__ === prototype); //true

有原型就有原理性了

構造器模式

在面向對象的編程語言中,構造器是一個類中用來初始化新對象的特殊方法。而且能夠接受參數用來設定實例對象的屬性的方法
function Car(model, year, miles) {
  this.model = model;
  this.year = year;
  this.miles = miles;
  // this.info = new CarDetail(model)
  // 屬性也能夠經過 new 的方式產生
}

// 覆蓋原型對象上的toString
Car.prototype.toString = function() {
  return this.model + " has done " + this.miles + " miles";
};

// 使用:
var civic = new Car("Honda Civic", 2009, 20000);
var mondeo = new Car("Ford Mondeo", 2010, 5000);
console.log(civic.toString()); // Honda Civic has done 20000 miles
console.log(mondeo.toString()); // Ford Mondeo has done 5000 miles

其實就是利用原型鏈上被繼承的特性,實現了構造器。

抽象工廠模式

抽象工廠模式(Abstract Factory)就是經過類的抽象使得業務適用於一個產品類簇的建立,而不負責某一類產品的實例。

JS 中是沒有直接的抽象類的,abstract 是個保留字,可是尚未實現,所以咱們須要在類的方法中拋出錯誤來模擬抽象類,若是繼承的子類中沒有覆寫該方法而調用,就會拋出錯誤。

const Car = function() {};
Car.prototype.getPrice = function() {
  return new Error("抽象方法不能調用");
};

面向對象的語言裏有抽象工廠模式,首先聲明一個抽象類做爲父類,以歸納某一類產品所須要的特徵,繼承該父類的子類須要實現父類中聲明的方法而實現父類中所聲明的功能:

/**
 * 實現subType類對工廠類中的superType類型的抽象類的繼承
 * @param subType 要繼承的類
 * @param superType 工廠類中的抽象類type
 */
const VehicleFactory = function(subType, superType) {
  if (typeof VehicleFactory[superType] === "function") {
    function F() {
      this.type = "車輛";
    }
    F.prototype = new VehicleFactory[superType]();
    subType.constructor = subType;
    subType.prototype = new F(); // 由於子類subType不只須要繼承superType對應的類的原型方法,還要繼承其對象屬性
  } else throw new Error("不存在該抽象類");
};
VehicleFactory.Car = function() {
  this.type = "car";
};
VehicleFactory.Car.prototype = {
  getPrice: function() {
    return new Error("抽象方法不可以使用");
  },
  getSpeed: function() {
    return new Error("抽象方法不可以使用");
  },
};
const BMW = function(price, speed) {
  this.price = price;
  this.speed = speed;
};
VehicleFactory(BMW, "Car"); // 繼承Car抽象類
BMW.prototype.getPrice = function() {
  // 覆寫getPrice方法
  console.log(`BWM price is ${this.price}`);
};
BMW.prototype.getSpeed = function() {
  console.log(`BWM speed is ${this.speed}`);
};
const baomai5 = new BMW(30, 99);
baomai5.getPrice(); // BWM price is 30
baomai5 instanceof VehicleFactory.Car; // true

經過抽象工廠,就能夠建立某個類簇的產品,而且也能夠經過 instanceof 來檢查產品的類別,也具有該類簇所必備的方法。

結構型

裝飾器模式

裝飾器模式,又名裝飾者模式。它的定義是「 在不改變原對象的基礎上,經過對其進行包裝拓展,使原有對象能夠知足用戶的更復雜需求 」。

裝飾器案例

有一個彈窗函數,點擊按鈕後會彈出一個彈框。

function openModal() {
  let div = document.craeteElement("div");
  div.id = "modal";
  div.innerHTML = "提示";
  div.style.backgroundColor = "gray";
  document.body.appendChlid(div);
}
btn.onclick = () => {
  openModal();
};

可是突然產品經理要改需求,要把提示文字由「提示」改成「警告」,背景顏色由 gray 改成 red。

聽到這個你是否是立馬就想直接改動源函數:

function openModal() {
  let div = document.craeteElement("div");
  div.id = "modal";
  div.innerHTML = "警告";
  div.style.backgroundColor = "red";
  document.body.appendChlid(div);
}

可是若是是複雜的業務邏輯,或者這個代碼時上任代碼留下來的產物,在考慮到之後的需求變化,每次都這樣修改確實很麻煩。

並且,直接修改已有的函數體,有違背了咱們的「開放封閉原則」,往一個函數塞這麼多的邏輯,也違背了「單一職責原則」,因此上面的方法並非最佳的。

最省時省力的方式是不去關心它現有得了邏輯,只在此邏輯之上擴展新的功能便可,所以裝飾器模式就此而生。

// 新邏輯
function changeModal() {
  let div = document.getElemnetById("modal");
  div.innerHTML = "告警";
  div.style.backgroundColor = "red";
}
btn.onclick = () => {
  openModal();
  changeModal();
};

這種經過函數添加新的功能、而又不修改舊邏輯,這就是裝飾器的魅力。

ES7 中的裝飾器

在最新的 ES7 中有裝飾器的提案,可是還未定案,因此語法可能不是最終版,可是思想是同樣的。

  1. 裝飾類的屬性
@tableColor
class Table {
  // ...
}
function tableColor(target) {
  target.color = "red";
}
Table.color; // true

Table這個類,添加一個tableColor的裝飾器,便可改變Tablecolor屬性

  1. 裝飾類的方法
class Person {
  @readonly
  name() {
    return `${this.first} ${this.last}`;
  }
}

Person類的name方法添加只讀的裝飾器,使得該方法不可被修改。

實際上是藉助Object.definePropertywirteable特性實現的。

  1. 裝飾函數

    由於 JS 中函數存在函數提高,直接使用裝飾器並不可取,可是可使用高級函數的方式實現。

    function doSomething(name) {
      console.log("Hello, " + name);
    }
    function loggingDecorator(wrapped) {
      return function() {
        console.log("fun-Starting");
        const result = wrapped.apply(this, arguments);
        console.log("fun-Finished");
        return result;
      };
    }
    const wrapped = loggingDecorator(doSomething);
    let name = "World";
    
    doSomething(name); // 裝飾前
    // output:
    // Hello, World
    
    wrapped(name); // 裝飾後
    // output:
    // fun-Starting
    // Hello, World
    // fun-Finished

    上面的裝飾器,是給一個函數在執行開始和執行結束分別打印一個 log。

參考

適配器模式

適配器模式的做用是解決兩個軟件實體間的接口不兼容問題。使用適配器模式以後,本來因爲接口不兼容而不能工做的兩個軟件實體能夠一塊兒工做。

簡單來講,就是把一個類的接口變成客戶端期待的另外一種接口,解決兼容問題

好比:axios

例子:一個渲染地圖的方法,默認是調用當前地圖對象的 show 方法進行渲染操做,當有多個地圖,而每一個地圖的渲染方法都不同時,爲了方便使用者調用,就須要作適配了。

let googleMap = {
  show: () => {
    console.log("開始渲染谷歌地圖");
  },
};
let baiduMap = {
  display: () => {
    console.log("開始渲染百度地圖");
  },
};
let baiduMapAdapter = {
  show: () => {
    return baiduMap.display();
  },
};
function renderMap(obj) {
  obj.show();
}
renderMap(googleMap); // 開始渲染谷歌地圖
renderMap(baiduMapAdapter); // 開始渲染百度地圖

這其中對「百度地圖」作了適配的處理。

小結

  • 適配器模式主要解決兩個接口之間不匹配的問題,不會改變原有的接口,而是由一個對象對另外一個對象的包裝
  • 適配器模式符合開放封閉原則
  • 把變化留給本身,把統一留給用戶。

代理模式

代理模式——在某些狀況下,出於種種考慮/限制,一個對象不能直接訪問另外一個對象,須要一個第三者(代理)牽橋搭線從而間接達到訪問目的,這樣的模式就是代理模式。

提起代理(Proxy),對於前端很熟悉的,我能聯想到一系列的東西,好比:

  • ES6 新增的 proxy 屬性
  • 爲了解決跨域問題而常用的 webpack 的 proxy 配置和 Nginx 代理
  • 還有「學會上網」所使用的的代理。
  • 等等

事件代理

常見的列表、表格都須要單獨處理事件時,使用父級元素事件代理,能夠極大的減小代碼量。

<div id="father">
  <span id="1">新聞1</span>
  <span id="2">新聞2</span>
  <span id="3">新聞3</span>
  <span id="4">新聞4</span>
  <span id="5">新聞5</span>
  <span id="6">新聞6</span>
  <!-- 七、8... -->
</div>

如上代碼,我想點擊每一個新聞,均可以拿到當前新聞的id,從而進行下一步操做。

若是給每個span都綁定一個onclick事件,就太耗費性能了,並且寫起來也很麻煩。

咱們常見的作法是利用事件冒泡的原理,將事件帶代理到父元素上,而後統一處理。

let father = document.getElementById("father");
father.addEventListener("click", (evnet) => {
  if (event.target.nodeName === "SPAN") {
    event.preventDefault();
    let id = event.target.id;
    console.log(id); // 拿到id,進行下一步操做
  }
});

虛擬代理

例如:某個花銷很大的操做,能夠經過虛擬代理的方式延遲到這種須要它的時候纔去建立(例如:使用虛擬代理實現圖片懶加載)

圖片預加載:先經過一張 loading 圖佔位,而後經過異步的方式加載圖片,等圖片加載完成以後在使用原圖替換 loading 圖。

問什麼要使用預加載+懶加載?以淘寶舉例,商城物品圖片多之又多,一次所有請求過來這麼多圖片不管是對 js 引擎仍是瀏覽器自己都是一個巨大的工做量,會拖慢瀏覽器響應速度,用戶體驗極差,而預加載+懶加載的方式會大大節省瀏覽器請求速度,經過預加載率先加載佔位圖片(第二次及之後都是緩存中讀取),再經過懶加載直到要加載的真實圖片加載完成,瞬間替換。這種模式很好的解決了圖片一點點展示在頁面上用戶體驗差的弊端。

須知:圖片第一次設置 src,瀏覽器發送網絡請求;若是設置一個請求過的 src 那麼瀏覽器則會從緩存中讀取 from disk cache

class PreLoadImage {
  constructor(imgNode) {
    // 獲取真實的DOM節點
    this.imgNode = imgNode;
  }

  // 操做img節點的src屬性
  setSrc(imgUrl) {
    this.imgNode.src = imgUrl;
  }
}

class ProxyImage {
  // 佔位圖的url地址
  static LOADING_URL = "xxxxxx";

  constructor(targetImage) {
    // 目標Image,即PreLoadImage實例
    this.targetImage = targetImage;
  }

  // 該方法主要操做虛擬Image,完成加載
  setSrc(targetUrl) {
    // 真實img節點初始化時展現的是一個佔位圖
    this.targetImage.setSrc(ProxyImage.LOADING_URL);
    // 建立一個幫咱們加載圖片的虛擬Image實例
    const virtualImage = new Image();
    // 監聽目標圖片加載的狀況,完成時再將DOM上的真實img節點的src屬性設置爲目標圖片的url
    virtualImage.onload = () => {
      this.targetImage.setSrc(targetUrl);
    };
    // 設置src屬性,虛擬Image實例開始加載圖片
    virtualImage.src = targetUrl;
  }
}

ProxyImage 幫咱們調度了預加載相關的工做,咱們能夠經過 ProxyImage 這個代理,實現對真實 img 節點的間接訪問,並獲得咱們想要的效果。

在這個實例中,virtualImage 這個對象是一個「幕後英雄」,它始終存在於 JavaScript 世界中、代替真實 DOM 發起了圖片加載請求、完成了圖片加載工做,卻從未在渲染層面拋頭露面。所以這種模式被稱爲「虛擬代理」模式。

【點擊查看Demo】:虛擬代理-在線例子

緩存代理

緩存代理比較好理解,它應用於一些計算量較大的場景裏。在這種場景下,咱們須要「用空間換時間」——當咱們須要用到某個已經計算過的值的時候,不想再耗時進行二次計算,而是但願能從內存裏去取出現成的計算結果。

這種場景下,就須要一個代理來幫咱們在進行計算的同時,進行計算結果的緩存了。

例子:對參數求和函數進行緩存代理。

// addAll方法會對你傳入的全部參數作求和操做
const addAll = function() {
  console.log("進行了一次新計算");
  let result = 0;
  const len = arguments.length;
  for (let i = 0; i < len; i++) {
    result += arguments[i];
  }
  return result;
};

// 爲求和方法建立代理
const proxyAddAll = (function() {
  // 求和結果的緩存池
  const resultCache = {};
  return function() {
    // 將入參轉化爲一個惟一的入參字符串
    const args = Array.prototype.join.call(arguments, ",");

    // 檢查本次入參是否有對應的計算結果
    if (args in resultCache) {
      // 若是有,則返回緩存池裏現成的結果
      console.log("無計算-使用緩存的數據");
      return resultCache[args];
    }
    return (resultCache[args] = addAll(...arguments));
  };
})();

let sum1 = proxyAddAll(1, 2, 3); // 進行了一次新計算

let sum2 = proxyAddAll(1, 2, 3); // 無計算-使用緩存的數據

第一次進行計算返回結果,並存入緩存。若是再次傳入相同的參數,則不計算,直接返回緩存中存在的結果。

在常見在 HTTP 緩存中,瀏覽器就至關於進行了一層代理緩存,經過 HTTP 的緩存機制控制(強緩存和協商緩存)判斷是否啓用緩存。

頻繁卻變化小的的網絡請求,好比getUserInfo,可使用代理請求,設置統一發送和存取。

小結

  • 代理模式符合開放封閉原則。
  • 本體對象和代理對象擁有相同的方法,在用戶看來並不知道請求的是本體對象仍是代理對象。

橋接模式

橋接模式:將抽象部分和具體實現部分分離,二者可獨立變化,也能夠一塊兒工做。

在這種模式的實現上,須要一個對象擔任「橋」的角色,起到鏈接的做用。

例子:

JavaScript 中橋接模式的典型應用是:Array對象上的forEach函數。

此函數負責循環遍歷數組每一個元素,是抽象部分; 而回調函數callback就是具體實現部分

下方是模擬forEach方法:

const forEach = (arr, callback) => {
  if (!Array.isArray(arr)) return;

  const length = arr.length;
  for (let i = 0; i < length; ++i) {
    callback(arr[i], i);
  }
};

// 如下是測試代碼
let arr = ["a", "b"];
forEach(arr, (el, index) => console.log("元素是", el, "位於", index));
// 元素是 a 位於 0
// 元素是 b 位於 1

外觀模式

外觀模式(Facade Pattern)隱藏系統的複雜性,並向客戶端提供了一個客戶端能夠訪問系統的接口。這種類型的設計模式屬於結構型模式,它向現有的系統添加一個接口,來隱藏系統的複雜性。

這種模式涉及到一個單一的類,該類提供了客戶端請求的簡化方法和對現有系統類方法的委託調用。

例子

外觀模式即執行一個方法可讓多個方法一塊兒被調用。

涉及到兼容性,參數支持多個格式、環境等等.. 對外暴露統一的 api

好比本身封裝的事件對象包含了阻止冒泡和添加事件監聽的兼容方法:

const myEvent = {
    stop (e){
        if(typeof e.preventDefault() == 'function'){
            e.preventDefault();
        }
        if(typeof e.stopPropagation() == 'function'){
            e.stopPropagation()
        }
        // IE
        if(typeOd e.retrunValue === 'boolean'){
            e.returnValue = false
        }
        if(typeOd e.cancelBubble === 'boolean'){
            e.returnValue = true
        }
    }
    addEvnet(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),又叫部分總體模式,是用於把一組類似的對象看成一個單一的對象。

組合模式依據樹形結構來組合對象,用來表示部分以及總體層次。這種類型的設計模式屬於結構型模式,它建立了對象組的樹形結構。

這種模式建立了一個包含本身對象組的類。該類提供了修改相同對象組的方式。

例子

想象咱們如今手上有多個萬能遙控器,當咱們回到家中,按一下開關,下列事情將被執行

  • 開門
  • 開電腦
  • 開音樂
// 先準備一些須要批量執行的功能
class GoHome {
  init() {
    console.log("開門");
  }
}
class OpenComputer {
  init() {
    console.log("開電腦");
  }
}
class OpenMusic {
  init() {
    console.log("開音樂");
  }
}

// 組合器,用來組合功能
class Comb {
  constructor() {
    // 準備容器,用來防止未來組合起來的功能
    this.skills = [];
  }
  // 用來組合的功能,接收要組合的對象
  add(task) {
    // 向容器中填入,未來準備批量使用的對象
    this.skills.push(task);
  }
  // 用來批量執行的功能
  action() {
    // 拿到容器中全部的對象,才能批量執行
    this.skills.forEach((val) => {
      val.init();
    });
  }
}

// 建立一個組合器
let c = new Comb();

// 提早將,未來要批量操做的對象,組合起來
c.add(new GoHome()); // 添加'開門'命令
c.add(new OpenComputer()); // 添加'開電腦'命令
c.add(new OpenMusic()); // 添加'開音樂'命令

c.action(); // 執行添加的全部命令

小結

  • 組合模式在對象間造成樹形結構
  • 組合模式中對基本對象和組合對象被一致對待
  • 無需關心對象有多少層,調用時只須要在根部進行調用
  • 將多個對象的功能,組裝起來,實現批量執行

享元模式

享元模式(Flyweight Pattern)主要用於減小建立對象的數量,以減小內存佔用和提升性能。

這種類型的設計模式屬於結構型模式,它提供了減小對象數量從而改善應用所需的對象結構的方式。

特色

  • 共享內存(主要是考慮內存,而非效率)
  • 相同的數據(內存),共享使用

例子

好比常見的事件代理,經過將若干個子元素的事件代理到一個父元素,子元素共同使用一個方法。若是都綁定到<span>標籤,對內存開銷太大 。

<!-- 點擊span,拿到當前的span中的內容 -->
<div id="box">
  <span>1</span>
  <span>2</span>
  <span>3</span>
  <span>4</span>
</div>

<script>
  var box = document.getElementById("box");
  box.addEventListener("click", function(e) {
    let target = e.target;
    if (e.nodeName === "SPAN") {
      alert(target.innerHTML);
    }
  });
</script>

小結

  • 將相同的部分抽象出來
  • 符合開放封閉的原則

行爲型

迭代器模式

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

迭代器模式能夠把迭代的過程從業務邏輯中分離出來,在使用迭代器模式以後,及時不關心對象的內部構造,也能夠按照順序訪問其中的每一個元素。

簡單類說,它的目的就是去遍歷一個可遍歷的對象。

像 JS 中原生的 forEach、map 等方法都屬因而迭代器模式的一種實現,通常來講不用本身去實現迭代器。

在 JS 中有一種類數組的存在,他們沒有迭代方法,好比 nodeList、arguments 並不能直接使用迭代方法,須要使用 jQuery 的 each 方法或者將類數組裝換爲真正的數組在進行迭代。

而在最新的 ES6 中,對有隻要有 Iterator 接口的數據類型均可以使用 for..of..進行遍歷,而他的底層則是對 next 方法的反覆調用,具體參考阮一峯-Iterator 和 for...of 循環

例子

咱們能夠藉助 Iterator 接口本身實現一個迭代器。

class Creater {
  constructor(list) {
    this.list = list;
  }
  // 建立一個迭代器,也叫遍歷器
  createIterator() {
    return new Iterator(this);
  }
}
class Iterator {
  constructor(creater) {
    this.list = creater.list;
    this.index = 0;
  }
  // 判斷是否遍歷完數據
  isDone() {
    if (this.index >= this.list.length) {
      return true;
    }
    return false;
  }
  next() {
    return this.list[this.index++];
  }
}

var arr = [1, 2, 3, 4];
var creater = new Creater(arr);
var iterator = creater.createIterator();
console.log(iterator.list); // [1, 2, 3, 4]
while (!iterator.isDone()) {
  console.log(iterator.next());
  // 1
  // 2
  // 3
  // 4
}

小結

  1. JavaScript 中的有序數據集合有 Array,Map,Set,String,typeArray,arguments,NodeList,不包括 Object
  2. 任何部署了[Symbol.iterator]接口的數據均可以使用 for...of 循環遍歷
  3. 迭代器模式使目標對象和迭代器對象分離,符合開放封閉原則

訂閱/發佈模式(觀察者)

發佈/訂閱模式又叫觀察者模式,她定義對象間的一種一對多的依賴關係。當一個對象的狀態發生改變時,全部依賴他的對象都將獲得通知。在 JavaScrtipt 中,咱們通常使用時間模型來替代傳統的發佈/訂閱模式。

好比:Vue 中的雙向綁定和事件機制。

發佈/訂閱模式和觀察者模式的區別

  • 發佈者能夠直接處接到訂閱的操做,叫觀察者模式
  • 發佈者不直接觸及到訂閱者,而是由統一的第三方完成通訊操做,叫發佈/訂閱模式

    發佈訂閱模式和觀察者模式.png

例子

能夠本身實現一個事件總線,模擬$emit$on

class EventBus {
  constructor() {
    this.callbacks = {};
  }
  $on(name, fn) {
    (this.callbacks[name] || (this.callbacks[name] = [])).push(fn);
  }
  $emit(name, args) {
    let cbs = this.callbacks[name];
    if (cbs) {
      cbs.forEach((c) => {
        c.call(this, args);
      });
    }
  }
  $off(name) {
    this.callbacks[name] = null;
  }
}
let event = new EventBus();
event.$on("event1", (arg) => {
  console.log("event1", arg);
});

event.$on("event2", (arg) => {
  console.log("event2", arg);
});

event.$emit("event1", 1); // event1 1
event.$emit("event2", 2); // event2 2

策略模式

定義一系列的算法,把他們一個個封裝起來,並使他們能夠替換。

策略模式的目的就是將算法的使用和算法的實現分離開來。

一個策略模式一般由兩部分組成:

  • 一組可變的策略類:封裝了具體的算法,負責具體的計算過程
  • 一組不變的環境類:接收到請求後,隨後將請求委託到某個策略類

說明環境類要維持對某個策略對象的引用。

例子

經過績效等級計算獎金,能夠輕易的寫出以下的代碼:

var calculateBonus = function(performanceLevel, salary) {
  if (performanceLevel === "S") {
    return salary * 4;
  }
  if (performanceLevel === "A") {
    return salary * 3;
  }
  if (performanceLevel === "B") {
    return salary * 2;
  }
};

calculateBonus("B", 20000); // 輸出:40000
calculateBonus("S", 6000); // 輸出:24000

使用策略模式修改代碼:

var strategies = {
  S: (salary) => {
    return salary * 4;
  },
  A: (salary) => {
    return salary * 3;
  },
  B: (salary) => {
    return salary * 2;
  },
};
var calculateBonus = function(level, salary) {
  return strategies[level](salary);
};
console.log(calculateBonus("S", 200)); // 輸出:800
console.log(calculateBonus("A", 200)); // 輸出:600

狀態模式

狀態模式容許一個對象在其內部狀態改變的時候改變

狀態模式主要解決的是當控制一個對象狀態的條件表達式過於複雜時的狀況。把狀態的判斷邏輯轉移到表示不一樣狀態的一系列類中,能夠把複雜的判斷邏輯簡化。

例子

實現一個交通燈的切換。

點擊查看Demo:交通訊號燈-在線例子

這時候若是在加一個藍光的話,能夠直接添加一個藍光的類,而後添加 parssBtn 方法,其餘狀態都不須要變化。

小結

  • 經過定義不一樣的狀態類,根據狀態的改變而改變狀態的行爲,沒必要把大量的邏輯都寫在被操做對象的類中,並且容易增長新的狀態。
  • 符合開放封閉原則

解釋器模式

解釋器模式(Interpreter):給定一個語言,定義它的文法的一種表示,並定義一個解釋器,這個解釋器使用該表示來解釋語言中的句子。

用到的比較少,能夠參考兩篇文章來理解。

小結

  • 描述語言語法如何定義,如何解釋和編譯
  • 用於專業場景

中介者模式

中介者模式(Mediator Pattern)是用來下降多個對象和類之間的通訊複雜性。

這種模式提供了一箇中介類,該類一般處理不一樣類之間的通訊,並支持鬆耦合,使代碼易於維護

經過一箇中介者對象,其餘全部相關對象都經過該對象來通訊,而不是相互引用,但其中一個對象發生改變時,只須要通知中介者對象便可。

經過中介者模式能夠解除對象與對象以前的耦合關係。

例如:Vuex

middle-parttern.png

參考連接:JavaScript 中介者模式

小結

  • 將各關聯對象經過中介者隔離
  • 符合開放封閉原則
  • 減小耦合

訪問者模式

在訪問者模式(Visitor Pattern)中,咱們使用了一個訪問者類,它改變了元素類的執行算法。

經過這種方式,元素的執行算法能夠隨着訪問者改變而改變。

例子

經過訪問者調用元素類的方法。

// 訪問者
function Visitor() {
  this.visit = function(concreteElement) {
    concreteElement.doSomething(); // 誰訪問,就使用誰的doSomething()
  };
}
// 元素類
function ConceteElement() {
  this.doSomething = function() {
    console.log("這是一個具體元素");
  };
  this.accept = function(visitor) {
    visitor.visit(this);
  };
}
// Client
var ele = new ConceteElement();
var v = new Visitor();
ele.accept(v); // 這是一個具體元素

小結

  • 假如一個對象中存在着一些與本對象不相干(或者關係較弱)的操做,爲了不這些操做污染這個對象,則可使用訪問者模式來把這些操做封裝到訪問者中去。
  • 假如一組對象中,存在着類似的操做,爲了不出現大量重複的代碼,也能夠將這些重複的操做封裝到訪問者中去。

備忘錄模式

備忘錄模式(Memento Pattern)保存一個對象的某個狀態,以便在適當的時候恢復對象

例子

實現一個帶有保存記錄功能的」編輯器「,功能包括

  • 隨時記錄一個對象的狀態變化
  • 隨時能夠恢復以前的某個狀態(如撤銷功能)
// 狀態備忘
class Memento {
  constructor(content) {
    this.content = content;
  }
  getContent() {
    return this.content;
  }
}

// 備忘列表
class CareTaker {
  constructor() {
    this.list = [];
  }
  add(memento) {
    this.list.push(memento);
  }
  get(index) {
    return this.list[index];
  }
}

// 編輯器
class Editor {
  constructor() {
    this.content = null;
  }
  setContent(content) {
    this.content = content;
  }
  getContent() {
    return this.content;
  }
  saveContentToMemento() {
    return new Memento(this.content);
  }
  getContentFromMemento(memento) {
    this.content = memento.getContent();
  }
}

// 測試代碼
let editor = new Editor();
let careTaker = new CareTaker();

editor.setContent("111");
editor.setContent("222");
careTaker.add(editor.saveContentToMemento()); // 存儲備忘錄
editor.setContent("333");
careTaker.add(editor.saveContentToMemento()); // 存儲備忘錄
editor.setContent("444");

console.log(editor.getContent()); // 444
editor.getContentFromMemento(careTaker.get(1)); // 撤銷
console.log(editor.getContent()); // 333
editor.getContentFromMemento(careTaker.get(0)); // 撤銷
console.log(editor.getContent()); // 222

小結

  • 狀態對象與使用者分開(解耦)
  • 符合開放封閉原則

模板方法模式

在模板模式(Template Pattern)中,一個抽象類公開定義了執行它的方法的方式/模板。

它的子類能夠按須要重寫方法實現,但調用將以抽象類中定義的方式進行。

感受用到的不是不少,想了解的能夠點擊下面的參考連接。

參考:JavaScript 設計模式之模板方法模式

職責鏈模式

顧名思義,責任鏈模式(Chain of Responsibility Pattern)爲請求建立了一個接收者對象的鏈。

這種模式給予請求的類型,對請求的發送者和接收者進行解耦。

在這種模式中,一般每一個接收者都包含對另外一個接收者的引用。

若是一個對象不能處理該請求,那麼它會把相同的請求傳給下一個接收者,依此類推。

例子

公司的報銷審批流程:組長=》項目經理=》財務總監

// 請假審批,須要組長審批、經理審批、最後總監審批
class Action {
  constructor(name) {
    this.name = name;
    this.nextAction = null;
  }
  setNextAction(action) {
    this.nextAction = action;
  }
  handle() {
    console.log(`${this.name} 審批`);
    if (this.nextAction != null) {
      this.nextAction.handle();
    }
  }
}

let a1 = new Action("組長");
let a2 = new Action("項目經理");
let a3 = new Action("財務總監");
a1.setNextAction(a2);
a2.setNextAction(a3);
a1.handle();
// 組長 審批
// 項目經理 審批
// 財務總監 審批

// 將一步操做分爲多個職責來完成,一個接一個的執行,最終完成操做。

小結

  • 能夠聯想到 jQuery、Promise 這種鏈式操做
  • 發起者和處理者進行隔離
  • 符合開發封閉原則

命令模式

命令模式(Command Pattern)是一種數據驅動的設計模式,它屬於行爲型模式。

請求以命令的形式包裹在對象中,並傳給調用對象。

調用對象尋找能夠處理該命令的合適的對象,並把該命令傳給相應的對象,該對象執行命令。

例子

實現一個編輯器,有不少命令,好比:寫入、讀取等等。

class Editor {
  constructor() {
    this.content = "";
    this.operator = [];
  }
  write(content) {
    this.content += content;
  }
  read() {
    console.log(this.content);
  }
  space() {
    this.content += " ";
  }
  readOperator() {
    console.log(this.operator);
  }
  run(...args) {
    this.operator.push(args[0]);
    this[args[0]].apply(this, args.slice(1));
    return this;
  }
}

const editor = new Editor();

editor
  .run("write", "hello")
  .run("space")
  .run("write", "zkk!")
  .run("read"); // => 'hello zkk!'

// 輸出操做隊列
editor.readOperator(); // ["write", "space", "write", "read"]

小結

  • 下降耦合
  • 新的命令能夠很容易的添加到系統中

綜述

(1)面向對象最終的設計目標:

  • A 可擴展性:有了新的需求,新的性能能夠容易添加到系統中,不影響現有的性能,也不會帶來新的缺陷。
  • B 靈活性:添加新的功能代碼修改平穩地發生,而不會影響到其它部分。
  • C 可替換性:能夠將系統中的某些代碼替換爲相同接口的其它類,不會影響到系統。

(2)設計模式的好處:

  • A 設計模式令人們能夠更加簡單方便地複用成功的設計和體系結構。
  • B 設計模式也會使新系統開發者更加容易理解其設計思路。

(3)學習設計模式有三重境界(網上看到好屢次):

  • 第一重: 你學習一個設計模式就在思考我剛作的項目中哪裏能用到(手中有刀,心中無刀)
  • 第二重: 設計模式你都學完了,可是當遇到一個問題的時候,你發現有好幾種設計模式供你選擇,你無處下(手中有刀,心中也有刀)
  • 第三重:也是最後一重,你可能沒有設計模式的概念了,內心只有幾大設計原則,等用到的時候信手拈來(刀法的最高境界:手中無刀,心中也無刀)

結語

如下是摘抄自掘金小冊-JavaScript 設計模式核⼼原理與應⽤實踐的結語。

設計模式的征程,到此就告一段落了。但對各位來講,真正的戰鬥纔剛剛開始。設計模式的魅力,不在紙面上,而在實踐中。

學設計模式:

一在多讀——讀源碼,讀資料,讀好書;

二在多練——把你學到的東西還原到業務開發裏去,看看它是否 OK,有沒有問題?若是有問題,如何修復、如何優化?沒有一種設計模式是完美的,設計模式和人同樣,處在動態發展的過程當中,並非只有 GOF 提出的 23 種設計模式能夠稱之爲設計模式。

只要一種方案遵循了設計原則、解決了一類問題,那麼它均可以被冠以「設計模式」的殊榮。

在各位從設計模式小冊畢業之際,但願你們帶走的不止是知識,還有好的學習習慣、閱讀習慣。最重要的,是深挖理論知識的勇氣和技術攻關的決心。這些東西不是所謂「科班」的專利,而是一個優秀工程師的必須。

參考

來自九旬的原創: 博客原文連接
相關文章
相關標籤/搜索