深刻 JavaScript 設計模式,今後有了優化代碼的理論依據

1、設計模式綜述

我想不少和我同樣的朋友小時候都看過《天龍八部》,裏面的女主角王語嫣是個武學博才,但本身卻毫無實戰。好比段譽和慕容復交手時,她連連口述指導:"段郎,二龍爪手,搶珠三式,小心你的腰肋,注意你的氣戶穴。潘月偷心,扶手相望......",雖然看着感受都是一些最基本的拳腳功夫,但有解說在旁邊,到底仍是感受高大上了不少。沒錯,設計模式其實就和這些招數名差很少,不少模式都給人一種其實平時沒少用,可就是不知道原來這是一個專業招術...。但咱們確實須要從系統層面深刻理解一下這些經常使用的模式,不只能夠起到發散思惟的做用,同時也能夠指導咱們解決問題的能力。若是以前不多接觸過設計模式,那麼這篇文章但願能夠助力你一下,感謝關注和點贊。css

1.1 模式定義

設計模式的定義:在面向對象軟件設計過程當中針對特定問題的簡潔而優雅的解決方案。html

說白了,設計模式就是一種理念,經過一些設計思惟來解決平時編寫底層或業務代碼時遇到的場景問題。好比早期業務中的一個封裝類,同時帶有一些封裝方法。若是如今該類不能再知足所有業務場景,且不容許修改原方法,此時就須要裝飾器或適配器模式來解決;又好比當設計一個場景,在調用一個固定對象時必定要先執行某些方法,好比驗證登陸、驗證身份ID等場景,此時就應該用到代理模式。這種例子有不少,能夠先看一下設計模式的分類。前端

1.2 模式分類

設計模式,按標準劃分,有3大類23種,而因爲JavaScript的一些特性,如弱類型語言、無接口編程等特徵,故其中只有一些模式是比較重要的。下面給出這23種設計模式名稱。vue

類型 模式名稱
建立型 工廠 單例 原型
組合型(結構型) 適配器 裝飾器 代理 外觀 橋接
行爲型 觀察者 命令 中介者 狀態 策略 解釋器 迭代器 訪問者 模板方法 職責鏈 備忘錄

是否是以爲這些高逼格的詞彙很霸氣,下面就先從一些重要的模式開展瞭解和深刻。node

2、工廠模式

1.1 基本特徵

工廠模式有三種形式:簡單工廠模式(Simple Factory)、工廠方法模式(Factory Method)和抽象工廠模式(Abstract Factory)。在js中咱們最多見的當屬簡單工廠模式。工廠模式的設計思想即:npm

  • 將 new 操做單獨封裝,只對外提供相應接口;
  • 遇到new 時,就要考慮是否應該使用工廠模式;

1.2 核心做用

工廠模式的核心做用以下:編程

  • 主要用於隱藏建立實例的複雜度,只需對外提供一個接口;
  • 實現構造函數和建立者的分離,知足開放封閉的原則;

1.3 分類

  • 簡單工廠模式:一個工廠對象建立一種產品對象實例。即用來建立同一類對象;
  • 工廠方法模式:創建抽象核心類,將建立實例的實際重心放在覈心抽象大類的子類中;
  • 抽象工廠模式:對類的工廠抽象用來建立產品類簇,不負責建立某一類產品的實例。 因爲在JS中基本不會使用抽象工廠模式,所以本文探究前兩類模式。

1.4 實例演示

先經過一個簡單例子最直觀感覺什麼是工廠:redux

// 定義產品
class Product {
    constructor(name) {
        this.name = name;
    }
    init() {
        console.log('初始化產品')
    }
}

// 定義工廠
class Factory {
    create(name) {
        return new Product(name); // 核心思想
    }
}

let c = new Factory(); 
let p = c.create('p1');
p.init();
複製代碼

工廠模式最直觀的地方在於,建立產品對象不是經過直接new 產品類實現,而是經過工廠方法實現。如今再用一個稍微有些好看的例子描述一下簡單工廠:設計模式

//User類
class User {
  //構造器
  constructor(opt) {
    this.name = opt.name;
    this.viewPage = opt.viewPage;
  }

  static getInstance(role) {
    switch (role) {
      case 'superAdmin':
        return new User({ name: '超級管理員', viewPage: ['首頁', '通信錄', '發現頁', '應用數據', '權限管理'] });
        break;
      case 'admin':
        return new User({ name: '管理員', viewPage: ['首頁', '通信錄'] });
        break;
      default:
        throw new Error('params error')
    }
  }
}

//調用
let superAdmin = User.getInstance('superAdmin');
let admin = User.getInstance('admin');
複製代碼

經過上例,咱們能夠看到,每次建立新的對象實例時,只須要傳入相應的參數,就能夠獲得指定的對象實例。最直觀的例子是若是不用工廠模式,那代碼中是否是就會多出好多個new,這樣看着也不太舒服。數組

其實簡單工廠模式已經能知足咱們前端大部分業務場景了,若是非要說其一個缺陷,那就是每次有新實例時,咱們須要重寫這個User大類,總歸感受和後面所述的裝飾器模式有一些衝突。此時,工廠方法模式就出來了,其核心思想就是獨立出一個大的User類,將建立實例對象的過程用其子類來實現:

class User {
  constructor(name = '', viewPage = []) {
    this.name = name;
    this.viewPage = viewPage;
  }
}

class UserFactory extends User {
  constructor(name, viewPage) {
    super(name, viewPage)
  }
  create(role) {
    switch (role) {
      case 'superAdmin': 
        return new UserFactory( '超級管理員', ['首頁', '通信錄', '發現頁', '應用數據', '權限管理'] );
        break;
      case 'admin':
        return new UserFactory( '管理員', ['首頁', '通信錄'] );
        break;
      default:
        throw new Error('params error');
    }
  }
}
let userFactory = new UserFactory();
let superAdmin = userFactory.create('superAdmin');
let admin = userFactory.create('admin');
let user = userFactory.create('user');
複製代碼

這樣,雖然也得經過 new 一個實例,但至少咱們能夠無需修改User類裏面的東西,雖然說代碼量上感受和簡單模式差不了多少,但思想主體確實就是這樣。

1.5 應用場景

(1) jQuery的選擇器$(selector)

$('div')new $('div') 有何區別? 爲何 $('div') 就能直接實現 new的效果,同時去除了 new $('div') 這種$('div') 去除了 new 書寫繁雜的弊端,還能實現完美的鏈式操做代碼簡介,就是由於$內置的實現機制是工廠模式。其底層代碼以下:

class jQuery {
    constructor(selector) {
        super(selector)
    }
    // ...
}

window.$ = function(selector) {
    return new jQuery(selector)
}
複製代碼

(2) Vue 異步組件

Vue.component('async-example' , (resolve , reject) => {
    setTimeout(function() {
        resolve({
            template: `<div>I am async!</div>`
        })
    }, 1000)
})
複製代碼

除了上述兩個常見的實例場景,還有React.createElement() 也是工廠原理。因此,當咱們平時遇到要建立實例的時候,就能夠想一想可否用工廠模式實現了。

3、單例模式

3.1 基本特徵

單例模式,顧名思義即保證明例在全局的單一性,概述以下:

  • 系統中被惟一使用
  • 一個類只有一個實例(注意只能有一個實例,必須是強相等===)

在平常業務場景中,咱們常常會遇到須要單例模式的場景,好比最基本的彈窗,或是購物車等。由於不管是在單頁面仍是多頁面應用程序中,咱們都須要這些業務場景只會同時存在一個。而若是用單例模式,則會避免須要外部變量來斷定是否存在的低端方法。

3.2 實例演示

舉一個單例模式的例子:

class Modal {
    login() {
        console.log('login...');
    }
}
Modal.create = (function() {
    let instance
    return function() {
        if(!instance) {
           instance = new Modal();
        }
        return instance
    }
})()
let m1 = Modal.create();
let m2 = Modal.create();
console.log(m1 === m2) // true
複製代碼

上述代碼是一種簡單版單例模式,經過js的當即執行函數和閉包函數,將初始實例肯定,以後即可經過斷定instance是否存在,果存在則直接返回,反之則建立了再返回,即確保一個類只有一個實例對象。還有一種種「透明版」單例模式:

let Modal = (function(){
    let instance;
    return function(name) {
        if (instance) {
           return instance;
        }
        this.name = name;
        return instance = this;
    }
})();

Modal.prototype.getName = function() {
    return this.name
}

let question = new Modal('問題框');
let answer = new Modal('回答框');

console.log(question === answer); // true
console.log(question.getName());  // '問題框'
console.log(answer.getName());  // '問題框'
複製代碼

因此,單例模式的實現實質即建立一個能夠返回對象實例的引用和一個獲取該實例的方法。保證建立對象的引用恆惟一。

3.3 應用場景

單例模式應用場景太多了 在Vue 中 咱們熟知的Vuex 和 redux 中的 store

3、適配器模式

3.1 定義及特徵

適配器模式很好理解,在平常開發中其實不經意間就用到了。適配器模式(Adapter)是將一個類(對象)的接口(方法或屬性)轉化成適應當前場景的另外一個接口(方法或屬性),適配器模式使得本來因爲接口不兼容而不能一塊兒工做的那些類(對象)能夠一些工做。因此,適配器模式必須包含目標(Target)、源(Adaptee)和適配器(Adapter)三個角色。

3.2 應用場景

舉個我工做中最生動簡單的例子,你就知道原來適配器無處不在。前端經過接口請求來一組數據集,類型分別文章、回答和課程,其中文章類返回的日期類型是2019-08-15 09:00:00格式字符串,回答類是2019/08/15 09:00:00,課程類返回的是時間戳格式,且文章、回答的建立時間字段叫createAt,課程叫createTime(咱們真就是這樣......)返回數據以下:

let result = [
      {
          id: 1
          type: 'Article',
          createAt: '2019-06-12 08:10:20',
          updateAt: '2019-08-15 09:00:00',
          ......
      },
      {
          id: 2
          type: 'Answer',
          createAt: '2019-04-11 08:11:23',
          updateAt: '2019/08/15 09:00:00',
          ......
      },
      {
          id: 3
          type: 'Course',
          createTime: 1554941483000,
          updateAt: 1565830800000,
          ......
      }
    ]
複製代碼

如今咱們要呈現這些實體的格式到移動端。並顯示一個統一的時間格式。而通常狀況下在遇到時間類型時,咱們一般首先想到的就是先 new Date() 一下,再作相應的轉換,可是很遺憾,在移動端IOS系統上,2019-08-15這種橫槓分隔格式的時間是不被識別的,因此,咱們此時就須要作個數據適配器作兼容處理:

let endResult = result.map(item => adapter(item));
 
 let adapter = function(item) {
    switch(item.type) {
        case 'Article':
          [item.createAt, item.updateAt] = [
             new Date(item.createAt.replace(/-/g,'/')).getTime(),
             new Date(item.updateAt.replace(/-/g,'/')).getTime()
          ]
        break;
        case: 'Answer': 
          item.createAt = new Date(item.createAt.replace(/-/g,'/')).getTime();
        break;
        case: 'Course':
          item.createAt = item.createTime
        break;
    }
 }
複製代碼

恩,沒錯,這個adapter 也能夠叫作數據適配器,有了這個方法,全部實體數據類型的數據就均可適配了。

再看一個基於ES6類的適配器例子:

// 目標
class Target {
    typeGB() {
        throw new Error('This method must be overwritten!');
    }
}

// 源
class Adaptee {
    typeHKB() {
        console.log("香港(Hong Kong)標準配件"); // 港獨都是sb
    }
}

// 適配器
class Adapter extends Target {
    constructor(adaptee) {
        super();
        this.adaptee = adaptee;
    }
    typeGB() {
        this.adaptee.typeHKB();
    }
}

let adaptee = new Adaptee();

let adapter = new Adapter(adaptee);
adapter.typeGB(); //香港(Hong Kong)標準配件
複製代碼

上述實例就將 Adaptee 類的實例對象的 typeHKB() 適配了通用的 typeGB() 方法。另外我不想重申官方說過的話,我只想直白一些:港..獨都是sb

4、裝飾器模式

4.1 定義及特徵

裝飾器,顧名思義,就是在原來方法的基礎上去裝飾一些針對特別場景所適用的方法,即添加一些新功能。所以其特徵主要有兩點:

  • 爲對象添加新功能;
  • 不改變其原有的結構和功能,即原有功能還繼續會用,且場景不會改變。

直接上個例子:

class Circle {
    draw() {
        console.log('畫一個圓形');
    }
}

class Decorator {
    constructor(circle) {
        this.circle = circle;
    }
    draw() {
        this.circle.draw();
        this.setRedBorder(circle);
    }
    setRedBorder(circle) {
        console.log('畫一個紅色邊框');
    }
}

let circle = new Circle();
let decorator = new Decorator(circle);
decorator.draw(); //畫一個圓形,畫一個紅色邊框
複製代碼

該例中,咱們寫了一個Decorator裝飾器類,它重寫了實例對象的draw方法,給其方法新增了一個setRedBorder(),所以最後爲其輸出結果進行了裝飾。

4.2 裝飾器插件

ES7 中就存在了裝飾器語法,須要安裝相應的babel插件,一塊兒看一下該插件如何用,首先安裝一下插件,並作相關的語法配置:

npm i babel-plugin-transform-decorators-legacy 

//.babelrc
{
    "presets": ["es2015", "latest"],
    "plugins": ["transform-decorators-legacy"]
}
複製代碼

給一個Demo類上添加一個裝飾器 testDec,此時 Demo類就具備了 裝飾器賦予的屬性:

@testDec
class Demo {}

function testDec(target) {
   target.isDec = true;
}

alert(Demo.isDec) // true
複製代碼

經過上例能夠得出下述代碼結論:

@decorator 
class A {}

// 等同於

class A {}
A = decorator(A) || A;
複製代碼

4.3 實力場景

裝飾器的實例場景有不少,咱們主要拿mixin和屬性裝飾學習一下。

(1) mixin 示例

function mixins(...list) {
   return function(target) {
      Object.assign(target.prototype, ...list)
   }
}

const Foo = {
    foo() {
        alert('foo');
    }
}

@mixins(Foo)
class MyClass { }

let obj = new MyClass();
obj.foo();
複製代碼

上例中,Foo做爲target的實參,MyClass做爲 list的實參,最終實現將Foo的全部原型方法(foo)裝飾到 MyClass類上,成爲了MyClass的方法。最終代碼的運行結果是執行了foo()

(2) 屬性裝飾器

固定語法:

function readonly(target, name, descriptor) {
    // descriptor 屬性描述對象(Object.defineProperty 中會用到)
    /*
      {
          value: specifiedFunction,
          enumerable: false,
          configurable: true
          writable: true 是否可改
      }
    */
}
複製代碼

設置類屬性只讀:

function readonly(target , name , descriptor) {
  descriptor.writable = false;
}

class Person {
    constructor() {
        this.first = '周';
        this.last = '杰倫';
    }

    @readonly
    name() {
        return `${this.first}${this.last}`
    }
}

const p = new Person();
console.log(p.name());  // 打印成功 ,‘周杰倫’

// 試圖修改name:
p.name = function() {
    return true;
}
// Uncaught TypeError:Cannot assign to read only property 'name' of object '#<Person>'
複製代碼

可見,再給屬性添加了只讀的裝飾後,代碼試圖修改屬性的命令將會報錯。

5、代理模式

5.1 定義及特徵

代理模式的定義以下:

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

通俗來講,代理模式要突出「代理」的含義,該模式場景須要三類角色,分別爲使用者、目標對象和代理者,使用者的目的是直接訪問目標對象,但卻不能直接訪問,而是要先經過代理者。所以該模式很是像明星代理人的場景。其特徵爲:

  • 使用者無權訪問目標對象;
  • 中間加代理,經過代理作受權和控制。

代理模式確實很方便,一般若是面臨一些很大開銷的操做,就能夠並採用虛擬代理的方式延遲到須要它的時候再去建立,好比懶加載操做。或者一些前置條件較多的操做,好比目標操做實現的前提必須是已登陸,且Id符合必定特徵,此時也能夠將這些前置判斷寫到代理器中。舉個加載圖片的例子:

class ReadImg {
    constructor(fileName) {
       this.fileName = fileName;
       this.loadFromDisk();
    }

    display() {
        console.log('display...' + this.fileName);
    }

    loadFromDisk() {
        console.log('loading...' + this.fileName);
    }
}

class ProxyImg {
    constructor(fileName) {
       this.readImg = new ReadImg(fileName)
    }

    display() {
        this.readImg.display();
    }
}

let proxyImg = new ProxyImg('1.png');
proxyImg.display();
複製代碼

5.2 實際應用

(1) HTML元素事件代理:

HTML元素代理事件,又名網頁代理事件,舉例以下:

<body>
    <div id="div1">
        <a href="#">a1</a>
        <a href="#">a2</a>
        <a href="#">a3</a>
        <a href="#">a4</a>
        <a href="#">a5</a>
    </div>

    <script>
       var div1 = document.getElementById('div1');
       div1.addEventListener('click', (e) => {
          var target = e.target;
          if(target.nodeName === 'A') {
             alert(target.innerHTML);
          }
       })
    </script>
</body>
複製代碼

該例中,咱們並未直接在元素上定義點擊事件,而是經過監聽元素點擊事件,並經過定位元素節點名稱來代理到<a>標籤的點擊,最終利用捕獲事件來實現相應的點擊效果。

(2) $.proxy

$.proxyjQuery 提供給咱們的一個代理方法,還以上述 html 元素爲例,寫一個點擊事件:

// html如上例
$('#div1').click(function() {
   setTimeout(function() {
      $(this).css('background-color', 'yellow')
   },1000)
})
複製代碼

上述div的點擊最終不會實現背景色變化,由於setTimeout的因素,致使內部函數中的this指向的是window而非相應的div。一般咱們的作法是在setTimeout方法前獲取當前this 指向,代碼以下:

$('#div1').click(function() {
   let _this = this;
   setTimeout(function() {
      $(_this).css('background-color', 'yellow')
   },1000)
})
複製代碼

而若是不用上面的方法,咱們就能夠用$.proxy代理目標元素來實現:

$('#div1').click(function() {
    var fn = $.proxy(function() {
        $(this).css('background-color', 'yellow')
    }, this);
    
    setTimeout(fn , 1000)
})
複製代碼

(3) ES6 proxy

ES6的 Proxy 相信你們都不會陌生,Vue 3.0 的雙向綁定原理就是依賴 ES6 的 Proxy 來實現,給一個簡單的例子:

let star = {
    name: '菜徐坤',
    song: '~雞你太美~'
    age: 40,
    phone: 13089898989
}

let agent = new Proxy(star , {
    get(target , key) {
        if(key == 'phone') {
            // 返回經濟人本身的電話
            return 15667096303
        }
        if(key == 'price') {
           return 20000000000
        }
        return target[key]
    },
    set(target , key , val) {
       if(key === 'customPrice') {
          if(val < 100000000) {
              throw new Error('價格過低')
          }
          else {
              target[key] = value;
              return true
          }
       }
    }
})

// agent 對象會根據相應的代理規則,執行相應的操做:
agent.phone // 15667096303  
agent.price // 20000000000 
複製代碼

不用多解釋了,真不明白他咋火的。。。。。。

7、觀察者模式

7.1 定義及特徵

觀察者模式有多重要?這麼說吧,若是上帝告訴你,這輩子你只能學習一種模式,你該絕不猶豫選擇觀察者模式。觀察者模式,也叫訂閱-發佈模式,熟悉Vue的朋友必定不會陌生,該模式定義了一種1對N的關係(注意:不必定是一對多,因此更準確地描述應該是1對N),使觀察者們同時監聽某一個對象相應的狀態變換,一旦變化則通知到全部觀察者,從而觸發觀察者相應的事件。所以,觀察者模式中的角色有兩類:觀察者(發佈者)和被觀察者(訂閱者)。

咱們可直接看一下觀察者模式的UML類圖:

image

類圖解析:

  • 每個觀察者(Observer)都有一個update 方法,而且觀察者的狀態就是等待被觸發;
  • 每個主題(subject)均可以經過attach方法接納N個觀察者所觀察,即觀察者們存儲在主題的observers數組裏,;
  • 主題有初始化狀態(init)、獲取狀態(getState)和設置狀態(setState)三個通用型方法;
  • 當主題的狀態發生變化時,經過特定的notifyAllObervers方法通知全部觀察者。

這下就很明白了,針對如上描述再來個小例子:

// 建立一個主題,保存狀態,狀態變化以後觸發全部觀察者對象
class Subject {
    constructor() {
        this.state = 0;
        this.observers = []
    }

    getState() {
        return this.state
    }

    setState(state) {
       this.state = state;
       this.notifyAllObservers()
    }

    notifyAllObservers() {
        this.observers.forEach(observer => {
            observer.update()
        })
    }

    attach(observer) {
       this.observers.push(observer)
    }
}

// 觀察者
class Observer {
    constructor(name , subject) {
       this.name = name;
       this.subject = subject;
       this.subject.attach(this);
    }
    update() {
        console.log(`${this.name} update, state: ${this.subject.getState()}`)
    }
}

let s = new Subject();
let o1 = new Observer('o1' , s);
let o2 = new Observer('o2' , s);
let o3 = new Observer('o3' , s);

s.setState(1)
s.setState(2)
s.setState(3)

/*
o1 update, state: 1
 o2 update, state: 1
o3 update, state: 1
o1 update, state: 2
o2 update, state: 2
o3 update, state: 2
o2 update, state: 3
o3 update, state: 3
*/
複製代碼

經過最終結果不能看到,主題每次改變狀態後都會觸發全部觀察者狀態更新,主題觸發了3次狀態,觀察者必定update了9次。

7.2 實例場景

其實咱們在平時不經意間就使用了不少觀察者模式的例子,好比Promise等、Node.js中的 EventEmitter事件監聽器、Vue 的 Watch生命週期鉤子等等,這些都是觀察者模式,好比在Vue組件生命週期Watch,爲甚在Watch裏設定了數據監聽,一旦數據改變了就觸發相應事件了?還有Promise,爲何異步操做獲得結果後就會進入到then或者catch裏呢?這些都依賴於觀察者模式。這裏我引用一篇很不錯的文章《vue的雙向綁定原理及實現》

好了,這篇文章的內容就先告一段落,咱們已經把23中設計模式中的核心重點都過了一遍,剩下的一些非重點,我會盡快整理出來,歡迎你們關注和點贊。

感謝千陽老師的校驗。

參考文章

相關文章
相關標籤/搜索