1. 程式人生 > >7 種 Javascript 常用設計模式學習筆記

7 種 Javascript 常用設計模式學習筆記

7 種 Javascript 常用設計模式學習筆記

由於 JS 或者前端的場景限制,並不是 23 種設計模式都常用。

有的是沒有使用場景,有的模式使用場景非常少,所以只是列舉 7 個常見的模式

本文的脈絡:

  • 設計與模式
  • 5 大設計原則
  • 7 種常見的設計模式

    • 一句話解釋含義
    • 列舉生活中的場景 、 業務程式碼場景
    • js 程式碼演示

設計與模式

之前一直以為「設計模式」是一個完整的名詞

其實「設計」和「模式」是要分開來說的

  • 「設計」:5 個常見的設計原則
  • 「模式」:程式碼中常見的"套路",被程式設計師總結成了相對固定的寫法,稱之為「模式」

也就是說學習"設計模式",首先肯定要學習和理解 5 個設計原則。

因為所有的模式,都是肯定是符合 5 大設計原則中的幾個,至少不違背設計原則的。

所以我們在學習模式時,不能上來就把 23 種模式的 js 實現看一遍,這樣是很難理解的。

我覺得正確的學習順序是:

  • 先學習 5 大設計原則
  • 再學習 23 種模式

而「模式」的學習順序是:

  • 1.嘗試用一句話概括這個模式。 (不理解也沒關係,至少對定義有一個大致印象)
  • 2.聯想在生活中的例子。(模式往往反映一種思想)
  • 3.更重要的是理解「模式」的程式碼中的應用(有的模式思想在生活中可能沒有合適的例子)
  • 4.最後才是用 js 程式碼去演示和實現「模式」

「模式」的學習步驟中,我覺得理解在程式碼中應用場景是最重要的

這也是為什麼所謂 23 種「模式」,這個文章只是大致講了 7 種。

因為我覺得沒有很多場景的模式,學習了意義也不大。

5 大設計原則

5 大設計原則,可能書和書的叫法不太一樣。

但是我覺得最重要的是,理解意思即可,畢竟實際上並不是所有「模式」都滿足 5 大設計原則

往往只是滿足 1、2 條,然後不違反其他原則,這樣。

  • 1.單一職責原則
  • 2.開放-封閉原則
  • 3.面向介面程式設計
  • 4.李氏置換原則
  • 5.介面獨立原則

個人理解 前 3 點 比較常見,後面的 2 個原則我也不太理解,或者可能前端場景下比較少見。

單一職責原則

一個函式只做一件事,如果功能過於複雜,最好讓一段程式碼只負責一部分邏輯。

開放-封閉原則

對拓展開放,對修改封閉。

  • 比如常見的 Vue 和 jQuery 都提供外掛拓展機制,你如果希望增加功能,可以拓展某一個技術棧的生態。而不是直接修改原始碼
面向介面程式設計

呼叫者時,只按照介面,不必清楚介面內部的類如何實現。

  • 比如前後分離開發時,前端只需要關注介面文件,不必瞭解具體實現
  • 比如設計 UI 元件時,我只需要考慮使用者需要如何呼叫,不用讓他操心,我的內部實現。

7 種常見的設計模式

我們要說的 7 種常見設計模式如下:

  • 工廠模式
  • 單例模式
  • 介面卡模式
  • 裝飾器模式
  • 代理模式
  • 觀察者模式
  • 迭代器模式

每 1 個模式的學習順序遵循以下 3 點

    1. 一句話概括這個模式的思想
    1. 生活場景的應用 和 業務場景的應用(重點理解業務場景)
    1. js 演示該模式

工廠模式

一句話描述: 工廠模式將 new 操作符封裝,拓展一個 create 的介面開發給呼叫者

業務場景

  • jQuery 的$()

window.$ = function(selector) {
    return new jQuery(selector);
};

比如 jQuery 把 $ 暴露給開發者,類似於 create 方法。

有了 $ 一般我們也不會使用 new jQuery() 來建立 jquery 物件

這樣做的好處:

  • $作為 create 寫起來更加簡單
  • 做了一層拓展,這樣暴露給使用者的是$,內部原始碼可以做各種修改,甚至不叫jQuery叫xQuery也可以,只要最終還是暴露$,對使用者來說就是一樣的。

符合開放-封閉原則

js 演示

class Product {
    constructor(options) {
        this.name = options.name;
        this.time = options.time;
        this.init();
    }
    init() {
        console.log(`產品名:${this.name} 保質期:${this.time}`);
    }
}
class Factory {
    create(options) {
        return new Product(options);
    }
}

let factory = new Factory();
let product1 = factory.create({ name: "麵包", time: "1個月" });

單例模式

一句話描述:一個類只有一個例項

業務場景

  • 場景 1: 比如 Vue 的外掛機制。Vue.use()多次,也只存在第一個外掛例項。
  • 場景 2: 比如 Vuex 的 Store 在例項化時,就算你例項化多個,也只存在一個 Store,這樣才能保證共享資料嘛。
  • 場景 3: 建立一個購物車元件。因為購物車往往整個專案只需要 1 個

比如 Vue.use 時,我們知道 Vue 原始碼中會去做判斷呼叫外掛的 install 方法

但是隻要 Vue.use 就直接呼叫的。

Vue 會把 Vue.use 的東西 push 到一個數組,每次執行 use 方法都會先檢查數組裡是否已經有這個外掛了,沒有就 push,有的話就不再執行後面的邏輯了。

這樣保證專案,這個外掛的 install 只初始化 1 次。

我覺得這就是單例模式思想的一種應用。來避免多次初始化可能造成的問題。

js 實現演示

實現思路

  • 給 SingleObject 新增一個靜態方法 getInstance
  • 將來例項化時,不是通過 new,而是 SingleObject.getInstance()
  • getInstance 內部的實現就是,第一次呼叫是用變數快取例項。之後呼叫時判斷該變數是否有值,沒有值就可以 new,有值就把這個變數 return

function SingleObject() {
    this.name = "單例";
}

SingleObject.getInstance = function() {
    if (!this.instance) {
        this.instance = new SingleObject();
    } 
    return this.instance;
    
};

var obj1 = SingleObject.getInstance();
var obj2 = SingleObject.getInstance();

console.log(obj1 === obj2);

介面卡模式

一句話描述: 介面不相容時,對舊介面做一層包裝,來適配新需求。

生活場景

插頭的電壓,不同國家是存在差異的嘛,經常有電源介面卡來做一層包裝,從而適配我們的電壓。

業務場景
  • Vue 的 computed 提供給函式和物件 2 種寫法。Vue 內部需要做一層適配
  • Node.js 中間層

比如 Vue 原始碼內部遍歷 computed 物件後,需要把使用者傳遞的函式作為該計算屬性的 getter 的返回值嘛

但是使用者有可能會傳遞 fn,也可能傳遞一個帶 get、set 方法的物件

那麼 Vue 面對使用者提供的不同介面,它就需要做一層適配。

無論函式直接提供函式,還是提供的物件,Vue 都轉化為一個函式

Vue 類似這種處理非常多,因為 Vue 提供給使用者很寬鬆的寫法嘛。比如你可以用 template 也可以用 render,但是最終 template 一定會被適配成 render 來呼叫

另一個例子,我覺得現在流行的 Node.js 中間層,也算是介面卡模式的應用。

後端提供一些基礎資料,但是移動端和 PC 端要求的資料格式是不同,且經常變化的。

Node.js 中間層的作用,可以讓後端的基礎資料不做變化,只是對資料在 Node 中再包裝一次,來適配具體的業務場景

js 實現演示

重點理解這個介面卡,在業務的應用。

感覺單純用 js 程式碼演示可能比較呆板


// 新增加的介面卡
class Adaptee {
    constructor() {
        this.name = "我是介面卡";
    }

    parse() {}
}


// 原來的舊程式碼
class OldApi{
    constructor(){
        this.name = '我是舊的介面'
        this.adaptee = new Adaptee()
        this.adaptee.parse()
    }
}

var oldApi = new OldApi()

這樣我們拓展了一個新的叫做介面卡的類,利用它來處理舊資料,而不是直接去具體修改舊資料

對拓展開發,對修改封閉,典型的符合開發-封閉的設計原則

裝飾器模式

一句話描述: 1.為物件裝飾一些新功能,2.舊的功能屬性全都保留

生活場景

手機殼對於手機,就是一種裝飾器

業務場景
  • ES7的裝飾器語法

下面介紹了「ES7的 @ 裝飾符」的使用,可以直接跳過。

  • 可以修飾類

例如


function decorator(target){
    target.type = '人類'
}

@decorator
class Animal{}

console.log(Animal.type)

不傳遞引數時 @ 就相當於 Animal = decorator(Animal) || Animal

也可以傳遞引數


function setType(type){
    return function(target){
        target.type = type
    }
}

@setType('人類')
class Animal{}

console.log(Animal.type)

傳遞引數時 @ 就相當於 Animal = setType(type)(Animal) || Animal

  • 也可以修飾類中的方法

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

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

readonly的3個引數含義如下

  • target: Person.prototype
  • name : key
  • descriptor : 描述器
js演示

實現東西

就是我有一個Circle類,用circle.draw()可以畫一個圓

經過裝飾後,可以畫一個帶紅色邊框的圓


class Circle {
    draw() {
        console.log("draw");
    }
}

class Decorator{
    constructor(circle) {
        this.circle = circle
    }

    setRedBorder() {
        console.log('border裝飾為紅色')
    }

    draw() {
        this.circle.draw()
        this.setRedBorder()
    }
}

let circle = new Circle()
let decorator = new Decorator(circle)

circle.draw()
decorator.draw()

符合單一職責原則,和開放-封閉原則

代理模式

一句話描述: 無法直接訪問時,通過代理來訪問目標物件

這裡區分一下介面卡模式、代理模式、裝飾器模式

    1. 介面卡模式是在原來的基礎上,做了一層包裝。雖然沒有動原來的介面,但最終我們是用包裝好的適配後的資料,肯定有修改的。
    1. 代理模式,是通過代理,訪問原資料。沒有什麼包裝和修改。
    1. 裝飾器模式,是原來的功能和函式都還要用的的基礎上,增加一些拓展功能。而適配的話是在包裝時做一些改造。
生活場景
  • FQ用的VPN
  • 海外代購
  • 各級代理商
業務場景
  • 繫結多個li時的事件代理
  • Vue的data、props被訪問時,就做了代理。
  • ES6的proxy的代理

ul下多個li,通過給父元素繫結click事件,實現對多個li的監聽。叫做事件代理

當然這裡能實現代理,也依靠了事件冒泡。但是不過是利用什麼機制做的代理。這種思想可以看做是代理的思想。

Vue的data,在Vue初始化時,this._init() ==> this.initState() ==> this.initData()

在initData中,除了遞迴迴圈把vm的data全都包裝為響應式物件之外,還呼叫了proxy()

這個proxy內部實現的,就是當你訪問this.msg時,實際上就訪問了this.data.msg

畢竟我們不可能真的把msg掛到Vue.prototype上。

這也是代理的思想。但是當然不是冒泡機制實現的,這裡是利用引用的傳遞。

ES6還提供了Proxy,被Proxy包裝的物件,也是一種代理。

原物件和包裝後的物件,很像明星和經紀人之間的關係

js演示

實現的效果就是proxyData.getName(),代理的是data.getName()



class Data{
    constructor(){
        this.name = '元資料'
    }

    getName(){
        console.log(this.name)
    }
}

class ProxyData{
    constructor(data){
        this.data = data
    }

    getName(){
        this.data.getName()
    }
}

let data = new Data()
let proxyData = new ProxyData(data)


data.getName()
proxyData.getName()

代理模式符合開放-封閉原則

觀察者模式

一句話描述:把watcher收集到一個佇列,等到釋出時再依次通知watcher,來實現非同步的一種模式。

生活場景

觀察者模式被廣泛的應用在日常生活和開發實踐中

  • 鬥魚主播是釋出者,觀眾但是訂閱者。
  • 獵頭是釋出者,候選人是訂閱者
  • 賽跑時,裁判開槍來發布,所有的運動員就是訂閱者。

這裡訂閱者也可以理解為觀察者。

業務場景
  • 1.Vue的收集依賴、派發更新
  • 2.瀏覽器事件機制
  • 3.Promise.then的非同步事件
  • 4.Vue的生命週期鉤子
  • 5.Node.js的eventEmitter
  • 場景1:Vue的收集依賴、派發更新

「訂閱」: Vue的響應式資料,在頁面渲染時,觸發getter收集watcher依賴。

「釋出」:資料變化時,觸發setter,Notify所有的watcher呼叫他們的update方法

  • 場景2:瀏覽器事件機制

「訂閱」: btn綁定了click事件,那個fn就被放到事件佇列中
「釋出」: 使用者點選時,觸發click事件。

  • 場景3 : Promise.then

「訂閱」: then呼叫時,把then的成功函式儲存在一個變數中

「釋出」: 當呼叫resolve時,狀態變化,並且把儲存的then的成功函式呼叫

  • 場景4: Vue的生命週期鉤子

「訂閱」: 在option上寫beforeCreate對應的函式

「釋出」: 當元件初始化階段,到了對應時機,vue幫你呼叫使用者提供的函式

js實現一個eventEmiiter


class EventEmitter {
    constructor() {
        this.eventMap = {};
    }

    on( type, fn ) {
        if ( !this.eventMap[type] ) {
            this.eventMap[type] = []
        } 
        this.eventMap[type].push(fn)
    }

    emit( type, ...params ) {
        this.eventMap[type].forEach(fn => {
            fn(...params);
        });
    }

    off( type, fn ) {
        let list = this.eventMap[type];
        let atIndex = list.indexOf(fn);
        
        if (atIndex !== -1) {
            list.splice(atIndex, 1);
        }
    }
}

迭代器模式

一句話描述: 1.按順序訪問集合, 2.呼叫者不用關係內部的資料結構

ES6之後,我們知道除了陣列之外的有序集合挺多的

  • Array
  • Map
  • Set
  • nodelist
  • arguments

那陣列的迭代,或者遍歷可以用10幾種API

那麼如果要遍歷Map/Set或者nodelist呢。如果給每一種資料結構都搞一套API就會很麻煩

包括JQ都有each遍歷。

會導致api氾濫的,所以我們把遍歷這種行為,抽象成一種模式。

場景
  • ES6的iterator

    • 為什麼需要iterator
    • iterator是什麼

瞭解一下iterator的內容,如果已經瞭解可以直接跳過

為什麼需要iterator

因為js需要遍歷的資料結構越來越多,如果給每一個數據結構都搞一套API會很麻煩。

所以,需要有一個統一的介面,可以遍歷各種資料結構。

iterator是什麼

就是比如Array.prototype就有一個[Symbol.iterator]屬性

這個屬性對應的是一個函式,函式呼叫後,得到一個迭代器

我們用這個迭代器就可以遍歷各種符合iterator標準的資料結構啦。

怎麼遍歷呢? 迭代器有next,用while去迭代

比如


function each(data) {
    let iterator = data[Symbol.iterator]();

    let item = { done: false };
    while (item.done === false) {
        item = iterator.next();
        if ( item.done ) return item
        console.log(item)
    }
}

let arr = [1, 2, 3, 4];
let nodeList = document.querySelectorAll("p");
let m = new Map();
m.set("a", 100);
m.set("b", 100);

each(arr)
each(nodeList)
each(m)

那麼,難道每一個開發者,都需要自己封裝一個each方法嗎?

ES6提供的for...of就是類似於each,用來遍歷符合iterator介面的資料結構


for ( let v of nodeList ) {
    console.log(v)
}
迭代器模式的js實現

這個要關注實現思路。

這裡只是把陣列設計成迭代器,有一個next方法和hasNext方法

需要有2個類

  • 1.Wrapper類,提供一個getIterator方法,可以生成一個迭代器
  • 2.Iterator類,迭代器。有next和hasNext方法

class Iterator{
    constructor(wrapper) {
        this.list = wrapper.list
        this.index = 0
    }

    next() {
        if ( this.hasNext() ) {
            return this.list[this.index++]
        } else {
            return null            
        }
    }

    hasNext() {
        return this.index < this.list.length
    }
}


class Wrapper {
    constructor(list) {
        this.list = list
    }

    getIterator(iterator) {
        return new Iterator(this)
    }
}

var arr = [1, 2, 3]
var iterator = new Wrapper( arr ).getIterator()
while ( iterator.hasNext() ) {
    console.log(iterator.next())
}

原文地址:https://segmentfault.com/a/1190000017145504