不知道怎麼封裝代碼?看看這幾種設計模式吧!

爲何要封裝代碼?

咱們常常據說:「寫代碼要有良好的封裝,要高內聚,低耦合」。那怎樣纔算良好的封裝,咱們爲何要封裝呢?其實封裝有這樣幾個好處:javascript

  1. 封裝好的代碼,內部變量不會污染外部。
  2. 能夠做爲一個模塊給外部調用。外部調用者不須要知道實現的細節,只須要按照約定的規範使用就好了。
  3. 對擴展開放,對修改關閉,即開閉原則。外部不能修改模塊,既保證了模塊內部的正確性,又能夠留出擴展接口,使用靈活。

怎麼封裝代碼?

JS生態已經有不少模塊了,有些模塊封裝得很是好,咱們使用起來很方便,好比jQuery,Vue等。若是咱們仔細去看這些模塊的源碼,咱們會發現他們的封裝都是有規律可循的。這些規律總結起來就是設計模式,用於代碼封裝的設計模式主要有工廠模式建立者模式單例模式原型模式四種。下面咱們結合一些框架源碼來看看這四種設計模式:vue

工廠模式

工廠模式的名字就很直白,封裝的模塊就像一個工廠同樣批量的產出須要的對象。常見工廠模式的一個特徵就是調用的時候不須要使用new,並且傳入的參數比較簡單。可是調用次數可能比較頻繁,常常須要產出不一樣的對象,頻繁調用時不用new也方便不少。一個工廠模式的代碼結構以下所示:java

function factory(type) {
  switch(type) {
    case 'type1':
      return new Type1();
    case 'type2':
      return new Type2();
    case 'type3':
      return new Type3();
  }
}

上述代碼中,咱們傳入了type,而後工廠根據不一樣的type來建立不一樣的對象。jquery

實例: 彈窗組件

下面來看看用工廠模式的例子,假如咱們有以下需求:git

咱們項目須要一個彈窗,彈窗有幾種:消息型彈窗,確認型彈窗,取消型彈窗,他們的顏色和內容多是不同的。github

針對這幾種彈窗,咱們先來分別建一個類:vue-router

function infoPopup(content, color) {}
function confirmPopup(content, color) {}
function cancelPopup(content, color) {}

若是咱們直接使用這幾個類,就是這樣的:設計模式

let infoPopup1 = new infoPopup(content, color);
let infoPopup2 = new infoPopup(content, color);
let confirmPopup1 = new confirmPopup(content, color);
...

每次用的時候都要去new對應的彈窗類,咱們用工廠模式改造下,就是這樣:數組

// 新加一個方法popup把這幾個類都包裝起來
function popup(type, content, color) {
  switch(type) {
    case 'infoPopup':
      return new infoPopup(content, color);
    case 'confirmPopup':
      return new confirmPopup(content, color);
    case 'cancelPopup':
      return new cancelPopup(content, color);
  }
}

而後咱們使用popup就不用new了,直接調用函數就行:框架

let infoPopup1 = popup('infoPopup', content, color);

改形成面向對象

上述代碼雖然實現了工廠模式,可是switch始終感受不是很優雅。咱們使用面向對象改造下popup,將它改成一個類,將不一樣類型的彈窗掛載在這個類上成爲工廠方法:

function popup(type, content, color) {
  // 若是是經過new調用的,返回對應類型的彈窗
  if(this instanceof popup) {
    return new this[type](content, color);
  } else {
    // 若是不是new調用的,使用new調用,會走到上面那行代碼
    return new popup(type, content, color);
  }
}

// 各類類型的彈窗所有掛載在原型上成爲實例方法
popup.prototype.infoPopup = function(content, color) {}
popup.prototype.confirmPopup = function(content, color) {}
popup.prototype.cancelPopup = function(content, color) {}

封裝成模塊

這個popup不只僅讓咱們調用的時候少了一個new,他其實還把相關的各類彈窗都封裝在了裏面,這個popup能夠直接做爲模塊export出去給別人調用,也能夠掛載在window上做爲一個模塊給別人調用。由於popup封裝了彈窗的各類細節,即便之後popup內部改了,或者新增了彈窗類型,或者彈窗類的名字變了,只要保證對外的接口參數不變,對外面都沒有影響。掛載在window上做爲模塊可使用自執行函數:

(function(){
 	function popup(type, content, color) {
    if(this instanceof popup) {
      return new this[type](content, color);
    } else {
      return new popup(type, content, color);
    }
  }

  popup.prototype.infoPopup = function(content, color) {}
  popup.prototype.confirmPopup = function(content, color) {}
  popup.prototype.cancelPopup = function(content, color) {}
  
  window.popup = popup;
})()

// 外面就直接可使用popup模塊了
let infoPopup1 = popup('infoPopup', content, color);

jQuery的工廠模式

jQuery也是一個典型的工廠模式,你給他一個參數,他就給你返回符合參數DOM對象。那jQuery這種不用new的工廠模式是怎麼實現的呢?其實就是jQuery內部幫你調用了new而已,jQuery的調用流程簡化了就是這樣:

(function(){
  var jQuery = function(selector) {
    return new jQuery.fn.init(selector);   // new一下init, init纔是真正的構造函數
  }

  jQuery.fn = jQuery.prototype;     // jQuery.fn就是jQuery.prototype的簡寫

  jQuery.fn.init = function(selector) {
    // 這裏面實現真正的構造函數
  }

  // 讓init和jQuery的原型指向同一個對象,便於掛載實例方法
  jQuery.fn.init.prototype = jQuery.fn;  

  // 最後將jQuery掛載到window上
  window.$ = window.jQuery = jQuery;
})();

上述代碼結構來自於jQuery源碼,從中能夠看出,你調用時省略的new在jQuery裏面幫你調用了,目的是爲了使大量調用更方便。可是這種結構須要藉助一個init方法,最後還要將jQueryinit的原型綁在一塊兒,其實還有一種更加簡便的方法能夠實現這個需求:

var jQuery = function(selector) {
  if(!(this instanceof jQuery)) {
    return new jQuery(selector);
  }
  
  // 下面進行真正構造函數的執行
}

上述代碼就簡潔多了,也能夠實現不用new直接調用,這裏利用的特性是this在函數被new調用時,指向的是new出來的對象,new出來的對象天然是類的instance,這裏的this instanceof jQuery就是true。若是是普通調用,他就是false,咱們就幫他new一下。

建造者模式

建造者模式是用於比較複雜的大對象的構建,好比VueVue內部包含一個功能強大,邏輯複雜的對象,在構建的時候也須要傳不少參數進去。像這種須要建立的狀況很少,建立的對象自己又很複雜的時候就適用建造者模式。建造者模式的通常結構以下:

function Model1() {}   // 模塊1
function Model2() {}   // 模塊2

// 最終使用的類
function Final() {
  this.model1 = new Model1();
  this.model2 = new Model2();
}

// 使用時
var obj = new Final();

上述代碼中咱們最終使用的是Final,可是Final裏面的結構比較複雜,有不少個子模塊,Final就是將這些子模塊組合起來完成功能,這種須要精細化構造的就適用於建造者模式。

實例:編輯器插件

假設咱們有這樣一個需求:

寫一個編輯器插件,初始化的時候須要配置大量參數,並且內部的功能不少很複雜,能夠改變字體顏色和大小,也能夠前進後退。

通常一個頁面就只有一個編輯器,並且裏面的功能可能很複雜,可能須要調整顏色,字體等。也就是說這個插件內部可能還會調用其餘類,而後將他們組合起來實現功能,這就適合建造者模式。咱們來分析下作這樣一個編輯器須要哪些模塊:

  1. 編輯器自己確定須要一個類,是給外部調用的接口
  2. 須要一個控制參數初始化和頁面渲染的類
  3. 須要一個控制字體的類
  4. 須要一個狀態管理的類
// 編輯器自己,對外暴露
function Editor() {
  // 編輯器裏面就是將各個模塊組合起來實現功能
  this.initer = new HtmlInit();
  this.fontController = new FontController();
  this.stateController = new StateController(this.fontController);
}

// 初始化參數,渲染頁面
function HtmlInit() {
  
}
HtmlInit.prototype.initStyle = function() {}     // 初始化樣式
HtmlInit.prototype.renderDom = function() {}     // 渲染DOM

// 字體控制器
function FontController() {
  
}
FontController.prototype.changeFontColor = function() {}    // 改變字體顏色
FontController.prototype.changeFontSize = function() {}     // 改變字體大小

// 狀態控制器
function StateController(fontController) {
  this.states = [];       // 一個數組,存儲全部狀態
  this.currentState = 0;  // 一個指針,指向當前狀態
  this.fontController = fontController;    // 將字體管理器注入,便於改變狀態的時候改變字體
}
StateController.prototype.saveState = function() {}     // 保存狀態
StateController.prototype.backState = function() {}     // 後退狀態
StateController.prototype.forwardState = function() {}     // 前進狀態

上面的代碼其實就將一個編輯器插件的架子搭起來了,具體實現功能就是往這些方法裏面填入具體的內容就好了,其實就是各個模塊的相互調用,好比咱們要實現後退狀態的功能就能夠這樣寫:

StateController.prototype.backState = function() {
  var state = this.states[this.currentState - 1];  // 取出上一個狀態
  this.fontController.changeFontColor(state.color);  // 改回上次顏色
  this.fontController.changeFontSize(state.size);    // 改回上次大小
}

單例模式

單例模式適用於全局只能有一個實例對象的場景,單例模式的通常結構以下:

function Singleton() {}

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

上述代碼中,Singleton類掛載了一個靜態方法getInstance,若是要獲取實例對象只能經過這個方法拿,這個方法會檢測是否是有現存的實例對象,若是有就返回,沒有就新建一個。

實例:全局數據存儲對象

假如咱們如今有這樣一個需求:

咱們須要對一個全局的數據對象進行管理,這個對象只能有一個,若是有多個會致使數據不一樣步。

這個需求要求全局只有一個數據存儲對象,是典型的適合單例模式的場景,咱們能夠直接套用上面的代碼模板,可是上面的代碼模板獲取instance必需要調getInstance才行,要是某個使用者直接調了Singleton()或者new Singleton()就會出問題,此次咱們換一種寫法,讓他可以兼容Singleton()new Singleton(),使用起來更加傻瓜化:

function store() {
  if(store.instance) {
    return store.instance;
  }
  
  store.instance = this;
}

上述代碼支持使用new store()的方式調用,咱們使用了一個靜態變量instance來記錄是否有進行過實例化,若是實例化了就返回這個實例,若是沒有實例化說明是第一次調用,那就把this賦給這個這個靜態變量,由於是使用new調用,這時候的this指向的就是實例化出來的對象,而且最後會隱式的返回this

若是咱們還想支持store()直接調用,咱們能夠用前面工廠模式用過的方法,檢測this是否是當前類的實例,若是不是就幫他用new調用就好了:

function store() {
  // 加一個instanceof檢測
  if(!(this instanceof store)) {
    return new store();
  }
  
  // 下面跟前面同樣的
  if(store.instance) {
    return store.instance;
  }
  
  store.instance = this;
}

而後咱們用兩種方式調用來檢測下:

image-20200521154322364

實例:vue-router

vue-router其實也用到了單例模式,由於若是一個頁面有多個路由對象,可能形成狀態的衝突,vue-router的單例實現方式又有點不同,下列代碼來自vue-router源碼

let _Vue;

function install(Vue) {
  if (install.installed && _Vue === Vue) return;
  install.installed = true

  _Vue = Vue
}

每次咱們調用vue.use(vueRouter)的時候其實都會去執行vue-router模塊的install方法,若是用戶不當心屢次調用了vue.use(vueRouter)就會形成install的屢次執行,從而產生不對的結果。vue-routerinstall在第一次執行時,將installed屬性寫成了true,而且記錄了當前的Vue,這樣後面在同一個Vue裏面再次執行install就會直接return了,這也是一種單例模式。

能夠看到咱們這裏三種代碼都是單例模式,他們雖然形式不同,可是核心思想都是同樣的,都是用一個變量來標記代碼是否已經執行過了,若是執行過了就返回上次的執行結果,這樣就保證了屢次調用也會拿到同樣的結果。

原型模式

原型模式最典型的應用就是JS自己啊,JS的原型鏈就是原型模式。JS中可使用Object.create指定一個對象做爲原型來建立對象:

const obj = {
  x: 1,
  func: () => {}
}

// 以obj爲原型建立一個新對象
const newObj = Object.create(obj);

console.log(newObj.__proto__ === obj);    // true
console.log(newObj.x);    // 1

上述代碼咱們將obj做爲原型,而後用Object.create建立的新對象都會擁有這個對象上的屬性和方法,這其實就算是一種原型模式。還有JS的面向對象其實更加是這種模式的體現,好比JS的繼承能夠這樣寫:

function Parent() {
  this.parentAge = 50;
}
function Child() {}

Child.prototype = new Parent();
Child.prototype.constructor = Child;      // 注意重置constructor

const obj = new Child();
console.log(obj.parentAge);    // 50

這裏的繼承其實就是讓子類Child.prototype.__proto__的指向父類的prototype,從而獲取父類的方法和屬性。JS中面向對象的內容較多,我這裏不展開了,有一篇文章專門講這個問題

總結

  1. 不少用起來順手的開源庫都有良好的封裝,封裝能夠將內部環境和外部環境隔離,外部用起來更順手。
  2. 針對不一樣的場景能夠有不一樣的封裝方案。
  3. 須要大量產生相似實例的組件能夠考慮用工廠模式來封裝。
  4. 內部邏輯較複雜,外部使用時須要的實例也很少,能夠考慮用建造者模式來封裝。
  5. 全局只能有一個實例的須要用單例模式來封裝。
  6. 新老對象之間可能有繼承關係的能夠考慮用原型模式來封裝,JS自己就是一個典型的原型模式。
  7. 使用設計模式時不要生搬硬套代碼模板,更重要的是掌握思想,同一個模式在不一樣的場景能夠有不一樣的實現方案。

文章的最後,感謝你花費寶貴的時間閱讀本文,若是本文給了你一點點幫助或者啓發,請不要吝嗇你的贊和GitHub小星星,你的支持是做者持續創做的動力。

做者博文GitHub項目地址: https://github.com/dennis-jiang/Front-End-Knowledges

做者掘金文章彙總:https://juejin.im/post/5e3ffc85518825494e2772fd

相關文章
相關標籤/搜索