JavaScript設計模式之結構型設計模式

Neal_yang發表於2017-12-26

github 全文地址 : YOU-SHOULD-KNOW-JS

JavaScript設計模式之外觀模式

概念

外觀模式:為一組複雜子系統介面提供一個更高階的統一介面,通過這個介面使得對子系統訪問更加的容易。

程式碼演示

// 使用外觀模式註冊事件監聽
function addEvent(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;
  }
}
// 使用外觀模式獲取事件物件

var getEvent = function(event) {
  return event || window.event;
}
複製程式碼

通過對介面的二次封裝,使其簡單易用,隱藏起內部的複雜度,外觀模式就是對介面的外層包裝,以供上層程式碼呼叫。因此外觀模式封裝的介面方法不需要介面的具體實現,只需要按照介面的使用規則使用即可

JavaScript設計模式之介面卡模式

概念

介面卡模式:將一個類的介面轉換為另外一個類的介面以滿足使用者的需求,使類之間的介面不相容問題通過介面卡得以解決。

程式碼演示

書中這裡說的比價沒意思,這裡我拿湯姆大叔的例子來說下

我們來舉一個例子,鴨子(Dock)有飛(fly)和嘎嘎叫(quack)的行為,而火雞雖然也有飛(fly)的行為,但是其叫聲是咯咯的(gobble)。如果你非要火雞也要實現嘎嘎叫(quack)這個動作,那我們可以複用鴨子的quack方法,但是具體的叫還應該是咯咯的,此時,我們就可以建立一個火雞的介面卡,以便讓火雞也支援quack方法,其內部還是要呼叫gobble。

首先要先定義鴨子和火雞的抽象行為,也就是各自的方法函式:

//鴨子
var Duck = function(){

};
Duck.prototype.fly = function(){
throw new Error("該方法必須被重寫!");
};
Duck.prototype.quack = function(){
throw new Error("該方法必須被重寫!");
}

//火雞
var Turkey = function(){

};
Turkey.prototype.fly = function(){
    throw new Error(" 該方法必須被重寫 !");
};
Turkey.prototype.gobble = function(){
    throw new Error(" 該方法必須被重寫 !");
};


//鴨子
var MallardDuck = function () {
    Duck.apply(this);
};
MallardDuck.prototype = new Duck(); //原型是Duck
MallardDuck.prototype.fly = function () {
    console.log("可以飛翔很長的距離!");
};
MallardDuck.prototype.quack = function () {
    console.log("嘎嘎!嘎嘎!");
};

//火雞
var WildTurkey = function () {
    Turkey.apply(this);
};
WildTurkey.prototype = new Turkey(); //原型是Turkey
WildTurkey.prototype.fly = function () {
    console.log("飛翔的距離貌似有點短!");
};
WildTurkey.prototype.gobble = function () {
    console.log("咯咯!咯咯!");
};
複製程式碼

為了讓火雞也支援quack方法,我們建立了一個新的火雞介面卡TurkeyAdapter:

var TurkeyAdapter = function(oTurkey){
    Duck.apply(this);
    this.oTurkey = oTurkey;
};
TurkeyAdapter.prototype = new Duck();
TurkeyAdapter.prototype.quack = function(){
    this.oTurkey.gobble();
};
TurkeyAdapter.prototype.fly = function(){
    var nFly = 0;
    var nLenFly = 5;
    for(; nFly < nLenFly;){
        this.oTurkey.fly();
        nFly = nFly + 1;
    }
};
複製程式碼

該建構函式接受一個火雞的例項物件,然後使用Duck進行apply,其介面卡原型是Duck,然後要重新修改其原型的quack方法,以便內部呼叫oTurkey.gobble()方法。其fly方法也做了一些改變,讓火雞連續飛5次(內部也是呼叫自身的oTurkey.fly()方法)。

    var oMallardDuck = new MallardDuck();
    var oWildTurkey = new WildTurkey();
    var oTurkeyAdapter = new TurkeyAdapter(oWildTurkey);
    
    //原有的鴨子行為
    oMallardDuck.fly();
    oMallardDuck.quack();
    
    //原有的火雞行為
    oWildTurkey.fly();
    oWildTurkey.gobble();
    
    //介面卡火雞的行為(火雞呼叫鴨子的方法名稱)
    oTurkeyAdapter.fly();
    oTurkeyAdapter.quack();
複製程式碼

JavaScript設計模式之代理模式

概念

代理模式:由於一個物件不能直接引用另一個物件,所以需要代理物件在這兩個物件之間起到中介的作用

程式碼演示

// 先宣告美女物件
var girl = function (name) {
    this.name = name;
};

// 這是dudu
var dudu = function (girl) {
    this.girl = girl;
    this.sendGift = function (gift) {
        alert("Hi " + girl.name + ", dudu送你一個禮物:" + gift);
    }
};

// 大叔是代理
var proxyTom = function (girl) {
    this.girl = girl;
    this.sendGift = function (gift) {
        (new dudu(girl)).sendGift(gift); // 替dudu送花咯
    }
};

var proxy = new proxyTom(new girl("酸奶小妹"));
proxy.sendGift("999朵玫瑰");
複製程式碼

假如dudu要送酸奶小妹玫瑰花,卻不知道她的聯絡方式或者不好意思,想委託大叔去送這些玫瑰,那大叔就是個代理

其實在日常開發中,我們遇到很多這種情況,比如跨域,之前總結過跨域的所有東西,其中的jsonp,window.name還是location.hash都是通過代理模式來實現的。

代理模式具體的從我的另一篇文章,JavaScript中的跨域總結去體會哈

JavaScript設計模式之裝飾著模式

概念

裝飾著模式,在不改變源物件的基礎上,通過對其進行包裝擴充使原有物件可以滿足使用者的更復雜需求

程式碼演示

這裡我拿給輸入框新增事件舉例

var decorator = function(input ,fn) {
  //獲取時間源
  var input = document.getElementById(input);
  if(typeof input.onclick === 'function'){
      //快取事件源原有的回撥函式
      var oldClickFn = input.onclick;
      input.onclick = function (ev) { 
          oldClickFn();
          fn();
       }
  }else{
      input.onclick = fn;
  }
}
複製程式碼

裝飾著模式很簡單,就是對原有物件的屬性和方法的新增。相比於之前說的介面卡模式是對原有物件的適配,新增的方法和原有的方法功能上大致相似。但是裝飾著提供的方法和原有方法功能項則有一定的區別,且不需要去了解原有物件的功能。只要原封不動的去使用就行。不需要知道具體的實現細節。

JavaScript設計模式之橋接模式

概念

橋接模式:在系統沿著多個維度變化的時候,不增加起復雜度已達到解耦的目的

應用場景

在我們日常開發中,需要對相同的邏輯做抽象的處理。橋接模式就是為了解決這類的需求。

橋接模式最主要的特點就是將實現層和抽象層解耦分離,是兩部分可以獨立變化

比如我們寫一個跑步遊戲,對於遊戲中的人和精靈都是動作單元。而他們的動作也是非常的統一。比如人和精靈和球運動都是x,y座標的改變,球的顏色和精靈的顏色繪製方式也非常的類似。 我們就可以將這些方法給抽象出來。

程式碼演示

//運動單元
function Speed(x,y) {
  this.x = x;
  this.y = y;
}
Speed.prototype.run = function() {
  console.log('動起來');
}
// 著色單元
function Color(cl) {
  this.color = cl;
}
Color.prototype.draw = function() {
  console.log('繪製色彩')
}

// 變形單元
function Shape(ap) {
  this.shape = ap;
}
Shape.prototype.change = function() {
  console.log('改變形狀');
}
//說話單元
function Speak(wd) {
  this.word = wd;
}
Speak.prototype.say = function() {
  console.log('請開始你的表演')
}


//建立球類,並且它可以運動可以著色
function Ball(x,y,c) {
  this.speed = new Speed(x,y);
  this.color = new Color(c);
}
Ball.prototype.init = function() {
  //實現運動和著色
  this.speed.run();
  this.color.draw();
}

function People(x,y,f) {
  this.speed = new Speed(x,y);
  this.speak = new Speak(f);
}

People.prototype.init = function() {
  this.speed.run();
  this.speak.say();
}
//...


//當我們例項化一個人物物件的時候,他就可以有對應的方法實現了

var p =new People(10,12,'我是一個人');
p.init();
複製程式碼

JavaScript設計模式之組合模式

概念

組合模式:又稱部分-整體模式,將物件組合成樹形結構以表示成“部分整體”的層次結構。組合模式使得使用者對單個物件以及組合物件的使用具有一致性

使用場景

我們平時開發過程中,一定會遇到這種情況:同時處理簡單物件和由簡單物件組成的複雜物件,這些簡單物件和複雜物件會組合成樹形結構,在客戶端對其處理的時候要保持一致性。比如電商網站中的產品訂單,每一張產品訂單可能有多個子訂單組合,比如作業系統的資料夾,每個資料夾有多個子資料夾或檔案,我們作為使用者對其進行復制,刪除等操作時,不管是資料夾還是檔案,對我們操作者來說是一樣的。在這種場景下,就非常適合使用組合模式來實現。

組合模式主要有三個角色:

(1)抽象元件(Component):抽象類,主要定義了參與組合的物件的公共介面

(2)子物件(Leaf):組成組合物件的最基本物件

(3)組合物件(Composite):由子物件組合起來的複雜物件

理解組合模式的關鍵是要理解組合模式對單個物件和組合物件使用的一致性,我們接下來說說組合模式的實現加深理解。

程式碼演示

// 抽象一個虛擬父類
var News = function() {
  this.children = [];
  this.element = null;
}

News.prototype = {
    init:function() {
      throw new Error('請重寫你的方法');
    },
    add:function() {
              throw new Error('請重寫你的方法');
            },
    getElement:function() {
                    throw new Error('請重寫你的方法');
                  },
}

function iniheritObject(o) {
  function F() {}
  F.prototype = o;
  return new F();
}

function inheritPrototype(subClass,superClass) {
  var p = iniheritObject(superClass.prototype);
  p.constructor = subClass;
  subClass.prototype = p;
}
//容器類
var Container = function(id,parent) {
  News.call(this);
  this.id = id;
  this.parent = parent;
  this.init();
}

//寄生式繼承父類原型方法
inheritPrototype(Container,News);

Container.prototype.init = function() {
  this.element = document.createElement('ul');
  this.element.id = this.id;
  this.element.className = 'new-container';
}

Container.prototype.add = function(child) {
  this.children.push(child);
  this.element.appendChild(child.getElement());
  return this;
}

Container.prototype.getElement = function() {
  return this.element;
}

Container.prototype.show = function() {
  this.parent.appendChild(this.element)
}
//同樣下一層極的行成員集合類以及後面新聞組合體類
var Item = function(classname) {
  News.call(this);
  this.classname = classname;
  this.init();
}
inheritPrototype(Item,News);
Item.prototype.init = function() {
  this.element = document.createElement('li');
  this.element.className = this.classname;
}
Item.prototype.add = function(child) {
  this.children.push(child);
  this.element.appendChild(child.getElement());
  return this;
}
Item.prototype.getElement = function() {
  return this.element;
}

var NewsGroup = function(className) {
  News.call(this);
  this.classname = classname|| '';
  this.init();
}
inheritPrototype(NewsGroup,News);
NewsGroup.prototype.init = function() {
  this.element = document.createElement('div');
  this.element.className = this.classname;
}
NewsGroup.prototype.add = function(child) {
  this.children.push(child);
  this.element.appendChild(child.getElement());
  return this;
}
NewsGroup.prototype.getElement = function() {
  return this.element;
}

複製程式碼

所以後面我們在使用的時候,建立新聞類,利用之前定義的組合元素去組合就可以了。

JavaScript設計模式之享元模式

概念

享元模式:運用共享技術有效的支援大量細粒度物件,避免物件之間擁有相同內容造成的不必要開銷

主要用來優化程式的效能,適合解決大量類似的物件產生的效能問題。享元模式通過分析應用程式的物件,將其解析為內在資料和外在資料,減少物件數量,從而提高程式的效能。

基礎知識

享元模式通過共享大量的細粒度的物件,減少物件的數量,從而減少物件的記憶體,提高應用程式的效能。其基本思想就是分解現有類似物件的組成,將其展開為可以共享的內在資料和不可共享的外在資料,我們稱內在資料的物件為享元物件。通常還需要一個工廠類來維護內在資料。

在JS中,享元模式主要有下面幾個角色組成:

  • 客戶端:用來呼叫享元工廠來獲取內在資料的類,通常是應用程式所需的物件
  • 享元工廠:用來維護享後設資料的類
  • 享元類:保持內在資料的類

基本實現

我們舉個例子進行說明:蘋果公司批量生產iphone,iphone的大部分資料比如型號,螢幕都是一樣,少數部分資料比如記憶體有分16G,32G等。未使用享元模式前,我們寫程式碼如下:

function Iphone(model, screen, memory, SN) {
    this. model  = model;
    this.screen = screen;
    this.memory = memory;
    this.SN = SN;
}
var phones = [];
for (var i = 0; i < 1000000; i++) {
    var memory = i % 2 == 0 ? 16 : 32;
    phones.push(new Iphone("iphone6s", 5.0, memory, i));
}
複製程式碼

這段程式碼中,建立了一百萬個iphone,每個iphone都獨立申請一個記憶體。但是我們仔細觀察可以看到,大部分iphone都是類似的,只是記憶體和序列號不一樣,如果是一個對效能要求比較高的程式,我們就要考慮去優化它。 大量相似物件的程式,我們就可以考慮用享元模式去優化它,我們分析出大部分的iphone的型號,螢幕,記憶體都是一樣的,那這部分資料就可以公用,就是享元模式中的內在資料,定義享元類如下:

 function IphoneFlyweight(model, screen, memory) {
      this.model = model;
      this.screen = screen;
      this.memory = memory;
  }
複製程式碼

我們定義了iphone的享元類,其中包含型號,螢幕和記憶體三個資料。我們還需要一個享元工廠來維護這些資料:

 var flyweightFactory = (function () {
     var iphones = {};
     return {
         get: function (model, screen, memory) {
             var key = model + screen + memory;
             if (!iphones[key]) {
                 iphones[key] = new IphoneFlyweight(model, screen, memory);
             }
             return iphones[key];
         }
     };
 })();
複製程式碼

在這個工廠中,我們定義了一個字典來儲存享元物件,提供一個方法根據引數來獲取享元物件,如果字典中有則直接返回,沒有則建立一個返回。 接著我們建立一個客戶端類,這個客戶端類就是修改自iphone類:

 function Iphone(model, screen, memory, SN) {
     this.flyweight = flyweightFactory.get(model, screen, memory);
     this.SN = SN;
 }
複製程式碼

然後我們依舊像之間那樣生成多個iphone

var phones = [];
for (var i = 0; i < 1000000; i++) {
    var memory = i % 2 == 0 ? 16 : 32;
    phones.push(new Iphone("iphone6s", 5.0, memory, i));
}
console.log(phones);
複製程式碼

這裡的關鍵就在於Iphone建構函式裡面的this.flyweight = flyweightFactory.get(model, screen, memory)。這句程式碼通過享元工廠去獲取享後設資料,而在享元工廠裡面,如果已經存在相同資料的物件則會直接返回物件,多個iphone物件共享這部分相同的資料,所以原本類似的資料已經大大減少,減少的記憶體的佔用。

在DOM中的使用

<ul class="menu">
    <li class="item">選項1</li>
    <li class="item">選項2</li>
    <li class="item">選項3</li>
    <li class="item">選項4</li>
    <li class="item">選項5</li>
    <li class="item">選項6</li>
</ul>
複製程式碼

點選選單項,進行相應的操作,我們通過jQuery來繫結事件,一般會這麼做:

 $(".item").on("click", function () {
     console.log($(this).text());
 })
複製程式碼

給每個列表項繫結事件,點選輸出相應的文字。這樣看暫時沒有什麼問題,但是如果是一個很長的列表,尤其是在移動端特別長的列表時,就會有效能問題,因為每個項都繫結了事件,都佔用了記憶體。但是這些事件處理程式其實都是很類似的,我們就要對其優化。

 $(".menu").on("click", ".item", function () {
     console.log($(this).text());
 })
複製程式碼

通過這種方式進行事件繫結,可以減少事件處理程式的數量,這種方式叫做事件委託,也是運用了享元模式的原理。事件處理程式是公用的內在部分,每個選單項各自的文字就是外在部分。我們簡單說下事件委託的原理:點選選單項,事件會從li元素冒泡到ul元素,我們繫結事件到ul上,實際上就繫結了一個事件,然後通過事件引數event裡面的target來判斷點選的具體是哪一個元素,比如低階第一個li元素,event.target就是li,這樣就能拿到具體的點選元素了,就可以根據不同元素進行不同的處理。

參考地址:http://luopq.com/2015/11/20/design-pattern-flyweight/

相關文章