一篇文章圖文並茂地帶你輕鬆學完 JavaScript 設計模式(一)

Huro~發表於2021-02-07

JavaScript 設計模式(一)

本文需要讀者至少擁有基礎的 ES6 知識,包括 Proxy, Reflect 以及 Generator 函式等。

至於這次為什麼分了兩篇文章,有損傳統以及標題的正確性,是這樣的。
其實放在一篇文章裡也可以,但是希望讀者能夠更加輕鬆點,文章太長也會導致陷入閱讀疲倦中。
因此希望讀者理解。

1. 工廠模式

JavaScript 寄生模式就是一種 工廠模式,具體可以參考我的關於 JavaScript 繼承 這篇文章,這裡不再細談寄生模式。

工廠模式是用工廠方法代替 new 的一種設計模式。

先看一個工廠模式的具體例子

class Product {
  constructor(name) {
    this.name = name;
  }
}

class Factory {
  static create(name) {
    return new Product(name);
  }
}

Factory.create("product1");
Factory.create("product2");

通過這種設計模式,我們可以少寫一個 new

jQuery 原始碼中,這種設計模式也有體現

$('#div'); // 我們會這樣傳入一個 selector 返回一個 jQuery.fn.init 物件

下面我們具體看原始碼中的內容,以下是我簡化過的原始碼

function jQuery(selector) {
  return new jQuery.fn.init(selector)
}

jQuery.fn = jQuery.prototype;				// 簡化 原型方法 書寫		
jQuery.fn.eat = function() {
  console.log(`${this.name} eat!`);
  return this;
}

const init = jQuery.fn.init = function(selector) {
  this.name = selector;
}

// 使得 jQuery.fn.init.prototype 與 jQuery.prototype 保持一致
// 用以使用 jQuery.prototype 即 jQuery.fn 上定義的方法或屬性
init.prototype = jQuery.prototype;

// 工廠模式
window.$ = function(selector) {
  return new jQuery(selector);
}

console.log($("huro").eat())

jQuery 實現的原始碼中,還是比較繞的,這種繞,其實這樣隱隱約約的實現了組合寄生繼承,分離了屬性和方法。

因為這個時候屬性例如 this.name 會在例項 new jQuery.fn.init() 上,

而這個例項的 __proto__ 指向 jQuery.prototype ,而我們是在 jQuery.prototype 上定義方法的,所以隱隱約約的,實現了屬性的獨立和方法的共享,節省了記憶體空間。

2. 單例模式

JavaScript 中沒有很好的單例模式的實現,究其原因,是因為沒有 private 關鍵字保護建構函式,現在最新的語法提案已經提出利用 # 字代表私有屬性或方法,可能幾年後就有了。如:

class Person {
    #name // 代表是一個私有屬性
}

目前單例模式我們一般這樣實現

class Singleton {
  eat() {
    console.log("huro eat!");
  }
}

Singleton.getInstance = (() => {
  let instance = null;
  return () => {
    if (instance === null) {
      instance = new Singleton();
    }
    return instance;
  };
})();

const obj1 = Singleton.getInstance();
const obj2 = Singleton.getInstance();
console.log(obj1 === obj2);
obj1.eat(); // huro eat!

這種設計模式在登入框或是註冊框,只要是單一使用的場景,可以應用。

class LoginForm {
  constructor() {
    this.display = "none";
  }
  show() {
    if (this.display === "block") {
      console.log("already show!");
    }
    else {
      this.display = "block";
    }
  }
  hide() {
    if (this.display === "none") {
      console.log("already hide!");
    }
    else {
      this.display = "none";
    }
  }
}

LoginForm.getInstance = (() => {
  let instance = null;
  return () => {
    if (instance === null) {
      instance = new LoginForm();
    }
    return instance;
  }
})();

const login1 = LoginForm.getInstance();
const login2 = LoginForm.getInstance();

console.log(login1 === login2);
login1.show();
login2.show(); // already show!

3. 觀察者模式

類似於釋出訂閱,實際上就是當被觀察者改變的時候通知觀察者。

但是觀察者模式是,觀察者主動去呼叫被觀察者的函式去觀察。

釋出訂閱模式是,觀察者(訂閱者)去找一箇中間商 (Bus) 去訂閱。被觀察者(釋出者)要釋出的時候也找那個中間商。只有中間商知道誰釋出了誰訂閱了,並及時推送資訊。

這裡借用柳樹的一張圖片,如果侵權,請聯絡我,我將立馬刪除。

具體觀察者模式實現如下

// 觀察者模式

// 被觀察者
class Subject {
  constructor() {
    this.state = 0;
    this.observers = [];
  }
  change(fn) {
    fn();
    this.notifyAll();
  }
  increase(num) {
    this.change(() => {
      this.state += num;
    })
  }
  multiply(num) {
    this.change(() => {
      this.state *= num;
    })
  }
  notifyAll() {
    this.observers.forEach(observer => {
      observer();
    })
  }
  observe(fn) {
    this.observers.push(fn);
  }
}

class Observer {
  constructor({
    subject,
    name,
    fn
  }) {
    subject.observe(fn);
    this.name = name;
  }
}

const subject = new Subject();

const ob1 = new Observer({
  name: 'ob1',
  subject,
  fn: () => console.log("ob1 observe object")
})

const ob2 = new Observer({
  name: 'ob2',
  subject,
  fn: () => console.log("ob2 observe object")
})

subject.increase(2);

4. 釋出訂閱模式

class Emitter {
  constructor() {
    this.map = new Map();
  }
  on(name, fn) {
    if (!this.map.has(name)) {
        this.map.set(name, []);
    }
    const origin = this.map.get(name);
    this.map.set(name, [...origin, fn]);
  }
  emit(name) {
    const events = this.map.get(name);
    if (events === undefined) {
      return;
    }
    events.forEach(fn => {
      fn();
    })
  }
}

const emitter = new Emitter();

emitter.on('click', () => {
  console.log("huro");
})

emitter.on('click', () => {
  console.log("huro");
})

emitter.on('mousemove', () => {
  console.log("huro");
})

emitter.emit('click'); // huro huro

感覺有那味道了,好像這種實現有點類似於瀏覽器的 addEventListener 只不過 emit 是由使用者的 click 等事件去觸發的。

總結

本文共介紹了四種設計模式,並在原始碼層面上給與了實現,部分設計模式也給出了相應的例子,下篇文章中,會繼續探討四種設計模式,分別是 代理模式,迭代器模式,裝飾器模式以及狀態模式,並結合 Promise 實現, 物件的 for of 迴圈等進行探討,歡迎讀者閱讀。

一篇文章圖文並茂地帶你輕鬆學完 JavaScript 設計模式(二)

相關文章