javascript設計模式總結

如下是js經常使用到的一些設計模式總結。html

1、設計模式分類

一、單例模式

定義: 保證一個類僅有一個實例,並提供一個訪問它的全局訪問點。js的常見單例對象有線程池、全局緩存、瀏覽器中的window對象git

實現原理: 用一個變量來標誌是否已經爲某個類建立過實例對象,若是建立過,這在下一次獲取該類的實例時,直接返回以前建立的實例對象。github

優勢:ajax

單例模式的代碼實現以下:算法

class Singleton{
    constructor(name){
        this.name=name;
        this.instance=null;
    }
    static getInstance(name){
        if(!this.instance){
            this.instance=new Singleton(name);
        }
        return this.instance;
    }
}
// 測試
let a=Singleton.getInstance("test");
let b=Singleton.getInstance("test");
console.log(a===b);//true
複製代碼

惰性單例模式: 將建立對象和管理單例的邏輯分開。編程

//將管理單例的邏輯封裝成一個方法
const getSingle=function(){
    let instance=null;
    return function(){
        return instance ||(instance=fn.apply(this,arguments))
    }
}
//建立對象
const  createLoginLayer=function(){
    let div = document.createElement( 'div' ); 
    div.innerHTML = '我是登陸浮窗';     
    div.style.display = 'none';     		
    document.body.appendChild( div );     
    return div;
}
let  createSingleLoginLayer = getSingle( createLoginLayer ); 
document.getElementById( 'loginBtn' ).onclick = function(){     
    let loginLayer = createSingleLoginLayer();     
    loginLayer.style.display = 'block'; 
}; 
複製代碼

二、策略模式

定義: 定義一系列的算法,把它們各自封裝成策略,算法被封裝在策略內部。根據不一樣參數來命中不一樣的策略。設計模式

//定義策略
let strategies={
    "S":salary=>salary*4,
    "A":salary=>salary*3,
    "B":salary=>salary*2,
    "C":salary=>salary,
}
//調用策略 
let calculateBonus=(level,salary)=> strategies[level](salary)

//測試
let sBonus=calculateBonus("S",40000);
let aBonus=calculateBonus("A",30000);
console.log(sBonus);//160000
console.log(aBonus);//90000
複製代碼

優勢數組

  • 用組合、委託和多態等技術和思想,能夠有效的避免多重條件選擇語句
  • 提供了對開放-閉合原則的完美支持,將算法封裝在獨立的strategy中,使得它們易於切換、易於理解,易於擴展。
  • 能夠避免許多重複的粘貼工做。
  • 是繼承的一種更輕便的替換方案。

缺點瀏覽器

  • 要向用戶暴露策略的全部實現,違反了最少知識原則。
  • 要使用策略模式,首先得了解全部的stratigy

三、代理模式

定義: 爲一個對象提供一個代用品或佔位符,以便控制對它的訪問。代理模式的關鍵點:當用戶直接訪問一個對象或者不知足需求的時候,提供一個替身對象對這個對象的訪問。緩存

代理模式分爲保護代理和虛擬代理

一、虛擬代理: 將一些開銷很大的對象,延遲到真正須要它的纔去建立。

1)、用虛擬代理實現圖片預加載

思路: 先用一張loading圖片佔位,而後用異步的方式加載圖片,等圖片加載好了再將它填充到img節點裏。

代理的意義: 負責預加載圖片,預加載的操做完成後,把請求交給本體myImage。符合單一職責原則——(一個類或者一個對象和函數),應該僅有一個引發它變化的緣由。

好處:

  • 一、用戶能夠放心的請求代理,只用關心可否獲得想要的結果。
  • 二、在任何使用本體的地方均可以替換成使用代理。
//功能:給img節點設置src
let myImage=(function(){
    let imfNode=document.createElement("img");
    document.body.appendChild(imgNode);
    return function(src) {
         imgNode.src=src;
    }
})();
//功能:圖片預加載
let proxyImage=(function(){
    //圖片預加載
    let img=new Image();
    img.onload=function(){
        //真正的圖片加載成功時觸發,此時的圖片資源已經下載好了
        myImage(this.src)
    }
    return function(src){
        //添加默認圖片佔位
        myImage("file:///E:/studyNotes/github/tangjie-93.github.io/images/git-branch.jpg")
        img.src=src;
    }
})();
proxyImage("http://pic44.nipic.com/20140723/18505720_094503373000_2.jpg");
複製代碼

2)、虛擬代理合並http請求

思路: 經過一個代理函數來收集一段時間內的請求,最後一次性的發送給服務器。

let synchronousFile=function(id){
    console.log("開始同步文件,id爲:"+id);
}
let proxySynchronousFile=(function(){
    let cache=[],timer;
    return function(id){
        if(timer){
            return;
        }
        timer=setTimeout(()=>{
            synchronousFile(cache.join(,));//
            clearTimeout(timer);
            timer=null;
            cache.length=0;
        },2000)
     }  
})()
document.getElementById("btn").onclick=function(){
    proxySynchronousFile(id);
}

複製代碼

二、保護代理: 代理幫助本體過濾掉一些請求。

let Flower=function(){};
let xiaoming={
    sendFlower:function(target){
        target.receiveFlower();
    }
}
//B屬於代理對象,能夠幫助A對象過濾一些請求
let B={
    receiveFlower:function(){
        //監聽A的好心情
        A.listenGoodMood(()=>{
            // new Flower()是一個大的開銷對象
            let flower=new Flower();
            A.receiveFlower(flower);
        })
    }
}
//目標對象
let A={
    receiveFlower:function(flower){
        console.log("收到花"+flower);
    },
    listenGoodMood:function(fn){
        //延遲10秒
        setTimeout(()=>{
            fn();
        },10000)
    }
}
xiaoming.sendFlower(B);
複製代碼

三、緩存代理

​ 緩存代理能夠爲一些開銷大的運算結果提供暫時的存儲,在下次運算時,若是傳遞進來的參數跟原來的一致,能夠直接返回以前存儲的運算結果。

實例:緩存乘積

let multi = function() {
  let a = 1;
  for (let i = 0, len = arguments.length; i < len; i++) {
    a = a * arguments[i];
  }
  return a;
};
let add = function() {
    let a = 1;
    for (let i = 0, len = arguments.length; i < len; i++) {
      a = a + arguments[i];
    }
    return a;
  };
let proxyFactory = function(fn) {
  let cache = new Map();
  return function() {
    let args = [].join.call(arguments, ",");
    if (!cache.has(args)) {
      cache.set(args, fn.apply(this, arguments));
    }
    return cache.get(args);
  };
};
let proxyMulti=proxyFactory(multi);
proxyMulti(1, 2, 3, 4); // 輸出:24
proxyMulti(1, 2, 3, 4); // 輸出:24
let proxyAdd=proxyFactory(add);
proxyAdd(1, 2, 3, 4); // 輸出:10
proxyAdd(1, 2, 3, 4); // 輸出:10
複製代碼

四、迭代器模式

定義: 指提供一種方法順序訪問一個聚合對象中的各個元素,而又不須要暴露該對象的內部表示。能夠分爲內部迭代器和外部迭代器。

好處: 能夠把迭代的過程從業務邏輯中分離出來,不用關心對象的內部構造,也能夠按順序訪問其中的每一個元素。

​ 內部迭代器:函數內部已經定義好了迭代規則,外部只須要一次初始調用。

let each=function(args,callback){
    for(let i=0,len=args.length;i<len;i++){
        callback.call(args[i],i,args[i])
    }
}
//測試
each([1,2,3],function(i,n){
    console.log(i,n)
})
//判斷兩個數組元素裏的值是否徹底相等
let compare=function(arr1,arr2){
    if(arr1.length!==arr2.length){
        throw new Error("arr1和arr2不相等")
    }
    each(arr1,(i,n)=>{
        if(n!==arr[i]){
           throw new Error("arr1和arr2不相等") 
        }
    })
    alert("arr1和arr2相等")
}
//測試
compare([1,2,3],[1,2,3,4])// Uncaught Error: arr1和arr2不相等
複製代碼

外部迭代器:必須顯示的請求迭代下一個元素。增長了調用的複雜度,同時也加強了迭代器的靈活性,能夠手動控制迭代的過程或者順序。

let Iterator=function(obj){
    let index=0;
    let next=function(){
        index+=1;
    };
    let isDone=function(){
        return index>=obj.length;
    };
    let getCurItem=function(){
        return obj[index];
    };
    return {
        next,
        isDone,
        getCurItem
    }
}
//判斷兩個數組元素裏的值是否徹底相等
let compare=function(iterator1,iterator2){
    while(!iterator1.isDone()&&!iterator2.isDone()){
        if(iterator1.getCurItem()!==iterator2.getCurItem()){
             throw new Error ( 'iterator1 和 iterator2 不相等' ); 
        }
        iterator1.next();
        iterator2.next(); 
    }
    alert ('iterator1 和 iterator2 相等'); 
}
var iterator1 = Iterator( [ 1, 2, 3 ] );
var iterator2 = Iterator( [ 1, 2, 3 ] ); 
compare( iterator1, iterator2 );  // iterator1 和 iterator2 相等 
複製代碼

停止迭代器

let each=function(arr,callback){
    for(let i=0;i<arr.length;i++){
        if(callback(i,arr[i])===false){
            break;
        }
    }
}
each([1,2,34,5,4,5],(i,n)=>{
    if(n>5){
        return false;
    }
    console.log(n);//輸出一、2
})
複製代碼

五、發佈訂閱模式

​ 又稱爲訂閱者模式,它定義了對象間的一種一對多的依賴關係,當一個對象的狀態發生改變時,全部依賴於它的對象都將獲得通知。

缺點

  • 建立訂閱者自己要消耗必定的時間和內存,當你訂閱一個消息後,也許此消息最後都未發生,可是這個訂閱者會始終存在內存中。
  • 能夠弱化對象之間的聯繫,可是過分使用的話,對象之間的必要聯繫也會被深藏在背後,會致使程序難以跟蹤維護和理解。

優勢

  • 時間上的解耦。

  • 對象之間的解耦

    實現步驟:

    • 首先指定誰充當發佈者。
    • 而後給發佈者添加一個緩存列表,用於存放回調函數以便通知訂閱者。
    • 最後發佈消息的時候,發佈者會遍歷這和緩存列表,一次觸發裏面存放的訂閱者回調函數。
class EventBus{
    constructor(){
        //中介者
        this.event=Object.create(null);
    };
    //註冊事件|監聽事件(訂閱者)
    on(name,fn){
        if(!this.event[name]){
            //一個事件可能有多個監聽者
            this.event[name]=[];
        };
        this.event[name].push(fn);
    };
    //觸發事件(觀察者)
    emit(name,...args){
        //給回調函數傳參
        this.event[name]&&this.event[name].forEach(fn => {
            fn(...args)
        });
    };
    //只被觸發一次的事件
    once(name,fn){
        //在這裏同時完成了對該事件的註冊、對該事件的觸發,並在最後取消該事件。
        const cb=(...args)=>{
            //觸發
            fn(...args);
            //刪除該訂閱者
            this.off(name,fn);
        };
        //監聽
        this.on(name,cb);
    };
    //取消事件
    off(name,offcb){
        if(this.event[name]){
            let index=this.event[name].findIndex((fn)=>{
                return offcb===fn;
            })
            this.event[name].splice(index,1);
            //沒有人訂閱該事件,則將該事件銷燬
            if(!this.event[name].length){
                delete this.event[name];
            }
        }
    }
}
複製代碼

六、命令模式

​ 命令模式是最簡單和優雅的模式之一,命令模式中的命令指的是一個執行某些特定事情的指令。

​ 應用場景:須要向某些對象發送請求,可是並不知道請求的接收者是誰,也不知道請求的操做是什麼。核心就是將請求發送者和接收者解耦

var closeDoorCommand = {     
    execute: function(){         
        console.log( '關門' );     
    } 
}; 
 
var openPcCommand = {     
    execute: function(){         
        console.log( '開電腦' );     
    } 
}
var openQQCommand = {     
    execute: function(){         
        console.log( '登陸 QQ' );     
    } 
}; 
//宏命令:命令模式和組合模式的產物。
var MacroCommand = function(){     
    return {         
        commandsList: [],         
        add: function( command ){             
            this.commandsList.push( command );         
        },         
        execute: function(){             
            for ( var i = 0, command; command = this.commandsList[ i++ ]; ){
                command.execute();             
            }         
        }     
    } 
}; 
 
var macroCommand = MacroCommand(); 
macroCommand.add( closeDoorCommand ); 
macroCommand.add( openPcCommand ); 
macroCommand.add( openQQCommand ); 
 
macroCommand.execute(); 
複製代碼

七、組合模式

​ 將對象組合成樹形結構,以表示"部分-總體"的層次結構。經過對象的多態性表現,使得用戶對單個對象和組合對象的使用具備一致性。

缺點

  • 系統中額每一個對象看起來和其餘對象都差很少。代碼在運行起來的時候區別纔會顯示出來,使得代碼難以理解。
  • 組合模式若是建立了太多的對象,可能會讓系統負擔不起。

優勢

  • 表示樹形結構:提供了一種遍歷樹形結構的方案,經過調用組合對象的execute方法,程序會遞歸調用組合對象下面的葉對象的execute方法。

  • 利用對象多態性統一的對待組合對象和單個對象。對象的多態性表現,能夠忽略組合對象和單個對象的不一樣。在組合模式中,不須要關心是組合對象仍是單個對象。

    組合模式使用場景

  • 表示對象的部分-總體層次結構。

  • 客戶但願統一對待樹中的全部對象。

let MacroCommand = function() {
      return {
        commandList: [],
        add: function(command) {
          this.commandList.push(command);
        },
        execute: function() {
          for (let i = 0,command;command=this.commandList[i++];) {
            command.execute();
          }
        }
      };
    };
    var openAcCommand = {
      execute: function() {
        console.log("打開空調");
      },
      add:function(){
           throw new Error( '葉對象不能添加子節點' ); 
      }
    };
    var openTvCommand = {
      execute: function() {
        console.log("打開電視");
      }
    };
    var openSoundCommand = {
      execute: function() {
        console.log("打開音響");
      }
    };
    //組合命令1
    var macroCommand1 = MacroCommand();
    macroCommand1.add(openTvCommand);
    macroCommand1.add(openSoundCommand);

    /*********關門、打開電腦和打登陸 QQ 的命令****************/

    var closeDoorCommand = {
      execute: function() {
        console.log("關門");
      }
    };

    var openPcCommand = {
      execute: function() {
        console.log("開電腦");
      }
    };

    var openQQCommand = {
      execute: function() {
        console.log("登陸 QQ");
      }
    };
    //組合命令2
    var macroCommand2 = MacroCommand();
    macroCommand2.add(closeDoorCommand);
    macroCommand2.add(openPcCommand);
    macroCommand2.add(openQQCommand);

    /*********如今把全部的命令組合成一個「超級命令」**********/
	//超集組合命令
    var macroCommand = MacroCommand();
    macroCommand.add(openAcCommand);
    macroCommand.add(macroCommand1);
    macroCommand.add(macroCommand2);
    /*********最後給遙控器綁定「超級命令」**********/

    var setCommand = (function(command) {
      document.getElementById("btn").onclick = function() {
        command.execute();
      };
    })(macroCommand);
複製代碼

利用組合模式掃描文件夾

class Folder {
    constructor(name) {
        this.name = name;
        this.files = [];
        this.parent=null;
    };
    add(file) {
        file.parent=this;
        this.files.push(file);
    };
    scan() {
        console.log("開始掃描文件夾:"+this.name)
        for (let i = 0, file; file = this.files[i++];) {
            file.scan();
        }
    };
    remove(){
        if(!this.parent){
            return;
        }
        for(let files=this.parent.files,len=files.length;len--;){
            let file=files[len];
            if(file===this){
                files.splice(len,1);
            }
        }
    }
}
//文件類
class File {
    constructor(name) {
        this.name = name;
        this.parent=null;
    };
    add() {
        throw new Error("文件下面不能再添加文件")
    };
    scan() {
        console.log("開始掃描文件")
        console.log("文件名爲:" + this.name)
    };
     remove(){
        if(!this.parent){
            return;
        }
        for(let files=this.parent.files,len=files.length;len--;){
            let file=files[len];
            if(file===this){
                files.splice(len,1);
            }
        }
    }
}
var folder = new Folder('學習資料'); 
var folder1 = new Folder('JavaScript'); 
var folder2 = new Folder('jQuery');

var file1 = new File('JavaScript 設計模式與開發實踐'); 
var file2 = new File('精通 jQuery'); 
var file3 = new File('重構與模式')


folder1.add(file1); 
folder2.add(file2);

folder.add(folder1); 
folder.add(folder2); 
folder.add(file3);
var folder3 = new Folder('Nodejs'); 
var file4 = new File('深刻淺出 Node.js');
folder3.add(file4);

var file5 = new File('JavaScript 語言精髓與編程實踐');
folder.add(folder3);
folder.add(file5);
folder1.remove();
folder.scan();
複製代碼

八、模板方法模式

​ 是一種只須要使用繼承就能夠實現的簡單模式。

​ 模板方法模式由兩部分結構組成,第一部分是抽象類,第二部分是具體的實現子類。抽象父類中封裝子類的算法框架,包括公共方法和子類中全部方法的執行順序。子類經過繼承抽象類,來繼承整個算法結構,也能夠選擇重寫父類的方法。

提示: 不少時候都不須要依樣畫瓢的去實現一個模板方法模式,高階函數式更好的選擇。

class Beverage {
    boilWater() {
        console.log("把水煮沸");
    };
    brew() {
        throw new Error("子類必須重寫brew方法");
    };
    pourInCup() {
        throw new Error("子類必須重寫pourInCup 方法");
    };
    addCondiments() {
        throw new Error("子類必須重寫 addCondiments 方法");
    };
    //鉤子函數
    customerWantsCondiments() {
        return true; // 默認須要調料
    };
    init() {
        this.boilWater();
        this.brew();
        this.pourInCup();
        if (this.customerWantsCondiments()) {
            // 若是掛鉤返回 true,則須要調料
            this.addCondiments();
        }
    }
}
class CoffeeWithHook extends Beverage {
    constructor() {
        super();
    };
    boilWater() {
        console.log("把水煮沸");
    };
    brew() {
        console.log("用沸水沖泡咖啡");
    };
    pourInCup() {
        console.log("把咖啡倒進杯子");
    };
    addCondiments() {
        console.log('加糖和牛奶' );
    };
    customerWantsCondiments() {
        return window.confirm("請問須要調料嗎?");
    }
}
var coffeeWithHook = new CoffeeWithHook(); 
coffeeWithHook.init(); 
複製代碼

九、享元模式

​ 享元(flyWeight)模式是一種用於性能優化的模式。其核心是運用共享技術來有效支持大量細粒度的對象。享元模式要求把對象的屬性劃分爲內部狀態和外部狀態(狀態也就是屬性)。其目標是儘可能減小共享對象的數量。

內部狀態和外部狀態的劃分原則:

  • 內部狀態存儲於對象內部。

  • 內部狀態被一些對象共享。

  • 內部狀態獨立於具體的場景,一般不會改變。

  • 外部狀態取決於具體的場景,並根據場景而變化,外部狀態不能被共享。

    享元模式的適用性

  • 一個程序中使用了大量的類似對象。

  • 因爲使用了大量對象,形成了很大的內存開銷。

  • 對象的大多數狀態均可以變爲外部狀態。

  • 剝離出對象的外部狀態以後,能夠用相對較少的共享對象取代大量對象。

一、享元模式之文件上傳

//建立共享對象(內部狀態)
class Upload {
    constructor(uploadType) {
        this.uploadType = uploadType;
    };
    delFile(id) {
        if (this.fileSize < 3000) {
            return this.dom.parentNode.removeChild(this.dom);
        }

        if (window.confirm("肯定要刪除該文件嗎? " + this.fileName)) {
            return this.dom.parentNode.removeChild(this.dom);
        }
    }
}
//建立上傳類工廠
class UploadFactory {
    constructor() {
        this.createdFlyWeightObjs = {};
    }
    //建立共享對象
    create(uploadType) {
        if (this.createdFlyWeightObjs[uploadType]) {
            return this.createdFlyWeightObjs[uploadType];
        }
        this.createdFlyWeightObjs[uploadType] = new Upload(uploadType)
        return this.createdFlyWeightObjs[uploadType] ;
    }
}
//封裝外部狀態
class UploadManager {
    constructor() {
        this.uploadDatabase = {};
    }
    add(id, uploadType, fileName, fileSize) {
        //建立享元對象
        const uploadObj=new UploadFactory();
        var flyWeightObj = uploadObj.create(uploadType);
        const dom=this.addDivDom(id,fileName,fileSize,flyWeightObj);
        this.uploadDatabase[id] = {
            fileName: fileName,
            fileSize: fileSize,
            dom: dom
        };

        return flyWeightObj;
    };
    addDivDom(id,fileName,fileSize,flyWeightObj){
        let dom = document.createElement("div");
        dom.innerHTML = `<span>文件名稱: ${fileName}, 文件大小:${fileSize} </span><button class="delFile">刪除</button>`;

        dom.querySelector(".delFile").onclick = ()=>{
            this.setExternalState(id, flyWeightObj);
            flyWeightObj.delFile(id);
        };
        document.body.appendChild(dom);
        return dom;
    };
    //設置外部狀態
    setExternalState(id, flyWeightObj) {
        var uploadData = this.uploadDatabase[id];
        Object.keys(uploadData).forEach(key=>{
            flyWeightObj[key] = uploadData[key];
        })
    }
}
let id = 0;
const startUpload = (uploadType, files) => {
    const uploadManager=new UploadManager();
    for (var i = 0, file; (file = files[i++]); ) {
        var uploadObj = uploadManager.add(
            ++id,
            uploadType,
            file.fileName,
            file.fileSize
        );
    }
};
startUpload("plugin", [
    { fileName: "1.txt", fileSize: 1000 },
    { fileName: "2.html", fileSize: 3000 },
    { fileName: "3.txt", fileSize: 5000 }
]);
startUpload("flash", [
    { fileName: "4.txt", fileSize: 1000 },
    { fileName: "5.html", fileSize: 3000 },
    { fileName: "6.txt", fileSize: 5000 }
]);
//有多少種內部狀態的組合,就有多少個共享對象。

複製代碼

二、對象池

​ 對象池也是一種性能優化方案,跟享元模式有一些類似之處,可是沒有分離內部狀態和外部狀態。

class ObjectPoolFactory {
    constructor(createObjFn) {
        this.objectPool = [];
    }
    create(createObjFn) {
        let obj =
            this.objectPool.length === 0
        ? createObjFn.apply(this, arguments)
        : this.objectPool.shift();

        return obj;
    };//回收節點
    recover(obj) {
        this.objectPool.push(obj);
    }
}
function createIframe() {
    var iframe = document.createElement("iframe");
    document.body.appendChild(iframe);

    iframe.onload = function() {
        iframe.onload = null;
        // 防止 iframe 重複加載的 bug
        new ObjectPoolFactory().recover(iframe);
        // iframe 加載完成以後回收節點
    };
    return iframe;
}
let ObjectPool= new ObjectPoolFactory();
let iframe1=ObjectPool.create(createIframe);

iframe1.src='http://baidu.com'; 
let iframe2=ObjectPool.create(createIframe);
iframe2.src='http://QQ.com'; 
let iframe3=ObjectPool.create(createIframe);
iframe3.src='http://QQ.com';

複製代碼

十、職責鏈模式

定義: 使多個對象都有機會處理請求,從而避免請求的發送者和接收者之間的耦合關係,將這些對象連城一條鏈,並沿着這條鏈傳遞該請求,直到有一個對象處理它爲止。

優勢:

  • 請求發送者只須要知道鏈中的第一個節點。弱化了發送者和一組接收者之間的強聯繫。
  • 鏈中的節點對象能夠靈活拆分重組。
  • 能夠手動指定起始節點,請求並不非得從鏈中的第一個節點開始傳遞。

缺點:

  • 咱們不能保證某個請求必定會被鏈中的節點處理。這種狀況能夠在鏈尾增長一個節點專門來處理異常。
  • 職責鏈可能會使得過程當中會增長許多沒有起到實質性做用的節點,會產生性能損耗。

職責鏈模式的經常使用場景: 早高峯坐公交投幣。(將硬幣往前傳遞給售票員)

實例:電商網站

class Chain{
     constructor(fn) {
         this.fn=fn;
         this.successor =null;
     };
     setNextSuccessor(successor ){
         //return 供鏈式調用
        return this.successor =successor 
     }
     passRequest(){
         //判斷執行結果是否是nextSuccessor,是的話,繼續往下執行
         const result=this.fn.apply(this,arguments);
         if(result==="nextSuccessor"){
             //遞歸,直到val不等於nextSuccessor爲止
             return this.successor&& this.successor.passRequest.apply(this.successor,arguments)
         }
         return val;
     }
 }
const order500=(orderType,pay,stock)=>{
    if(orderType==1&&pay===true){
        console.log("500 元定金預購, 獲得 100 優惠券");
        return;
    }else{
        return 'nextSuccessor'
    }
}
const order200=(orderType,pay,stock)=>{
    if(orderType==2&&pay===true){
        console.log("200 元定金預購, 獲得 50 優惠券");
        return;
    }else{
        return 'nextSuccessor'
    }
}
const orderNormal =(orderType,pay,stock)=>{
    if(stock>0){
        console.log( '普通購買,無優惠券' ); 
    }else{
        console.log( '手機庫存不足' ); 
    }
}
var chainOrder500 = new Chain( order500 ); 
var chainOrder200 = new Chain( order200 ); 
var chainOrderNormal = new Chain( orderNormal ); 
chainOrder500.setNextSuccessor( chainOrder200 )
    .setNextSuccessor( chainOrderNormal ); 
chainOrder500.passRequest( 1, true, 500 );    // 輸出:500 元定金預購,獲得 100 優惠券 
chainOrder500.passRequest( 2, true, 500 );    // 輸出:200 元定金預購,獲得 50 優惠券 
chainOrder500.passRequest( 3, true, 500 );    // 輸出:普通購買,無優惠券 
chainOrder500.passRequest( 1, false, 0 );     // 輸出:手機庫存不足 

複製代碼

用AOP實現職責鏈

const order500=(orderType,pay,stock)=>{
    if(orderType==1&&pay===true){
        console.log("500 元定金預購, 獲得 100 優惠券");
        return;
    }else{
        return 'nextSuccessor'
    }
}
const order200=(orderType,pay,stock)=>{
    if(orderType==2&&pay===true){
        console.log("200 元定金預購, 獲得 50 優惠券");
        return;
    }else{
        return 'nextSuccessor'
    }
}
const orderNormal =(orderType,pay,stock)=>{
    if(stock>0){
        console.log( '普通購買,無優惠券' ); 
    }else{
        console.log( '手機庫存不足' ); 
    }
}
//切換編程
Function.prototype.after = function( fn ){     
    var self = this;     
    return function(){         
        var ret = self.apply( this, arguments );         
        if ( ret === 'nextSuccessor' ){             
            return fn.apply( this, arguments );         
        } 
        return ret;     
    } 
}; 
var order = order500.after( order200 ).after( orderNormal ); 

order( 1, true, 500 );    // 輸出:500 元定金預購,獲得 100 優惠券 
order( 2, true, 500 );    // 輸出:200 元定金預購,獲得 50 優惠券 
order( 1, false, 500 );   // 輸出:普通購買,無優惠券 

複製代碼

十一、中介者模式

中介者模式的做用就是解除對象和對象之間的緊耦合關係。

優勢: 以中介者和對象的一對多關係取代了對象之間的網狀多對多關係。每一個對象只須要關注自身功能的實現便可。對象之間的交互關係交給中介者來實現和維護。

缺點: 系統中會增長一箇中介者對象,對象之間的交互複雜性,轉移成了中介者對象的複雜性,使得中介者對象自身會成爲一個難以維護的對象。

實例:用中介者模式實現泡泡堂遊戲

class Player {
      constructor(name, teamColor) {
        this.name =name;
        this.teamColor = teamColor;
        this.state = "alive";
        this.add();
      };
      win() {
        console.log(`玩家${this.name}贏了`);
      };
      lose() {
        console.log(`玩家${this.name}輸了`);
      };
      add(){
        if(!Player.playDirector){
          Player.playDirector=new PlayDirector();
        }    
        Player.playDirector.addPlayer(this);
      };
      die() {
        this.state = "dead";
        // 給中介者發送消息,玩家死亡
        Player.playDirector.playerDead(this);
      };
      remove() {
        console.log(`玩家${this.name}掉線了`);
        Player.playDirector.removePlayer(this);
      };
      changeTeam(color) {
        console.log(`玩家${this.name}叛變了`);
        // 給中介者發送消息,玩家換隊
        Player.playDirector.changeTeam(this, color);
      }
    }
    Player.playDirector=null;

    class PlayDirector {
      constructor() {
        this.players = {};
      }
      addPlayer(player) {
        let teamColor = player.teamColor; //玩家額隊伍顏色
        this.players[teamColor] = this.players[teamColor] || [];
        this.players[teamColor].push(player); //添加玩家
      }
      //移除玩家
      removePlayer(player) {
        let teamColor = player.teamColor;
        let teamPlayers = this.players[teamColor] || [];
        const index = teamPlayers.indexOf(player);
        index>-1 && teamPlayers.splice(index, 1);
      }
      //玩家換隊
      changeTeam(player, newTeamColor) {
        this.removePlayer(player); // 從原隊伍中刪除
        player.teamColor = newTeamColor; // 改變隊伍顏色 operations.addPlayer( player ); // 增長到新隊伍中
      }
      //玩家死亡
      playerDead(player) {
        // 玩家死亡
        var teamColor = player.teamColor,
          teamPlayers = this.players[teamColor]; // 玩家所在隊伍
        var all_dead = true;
        for (var i = 0, player; (player = teamPlayers[i++]); ) {
          if (player.state !== "dead") {
            all_dead = false;
            break;
          }
        }
        if (all_dead === true) {
         
          // 所有死亡
          for (var i = 0, player; (player = teamPlayers[i++]); ) {
            player.lose(); // 本隊全部玩家 lose
          }
          console.log(teamColor+"隊輸了")
          for (var color in this.players) {
            if (color !== teamColor) {
              var teamPlayers = this.players[color]; // 其餘隊伍的玩家
              for (var i = 0, player; (player = teamPlayers[i++]); ) {
                player.win(); // 其餘隊伍全部玩家 win
              }
            }
          }
        }
      }
    }
    //測試
    // 紅隊: 
    var player1 = new Player( '皮蛋', 'red' ),     
    player2 = new Player( '小乖', 'red' ),     
    player3 = new Player( '寶寶', 'red' ),     
    player4 =new Player( '小強', 'red' ); 
 
    // 藍隊: 
    var player5 = new Player( '黑妞', 'blue' ),     
    player6 = new Player( '蔥頭', 'blue' ),     
    player7 = new Player( '胖墩', 'blue' ),     
    player8 = new Player( '海盜', 'blue' ); 

    //紅隊所有死亡
    // player1.die(); 
    // player2.die(); 
    // player3.die(); 
    // player4.die();

    //玩家 player1和player1掉線
    // player1.remove();
    // player2.remove(); 
    // player3.die(); 
    // player4.die(); 
    
    //玩家1叛變
    player1.changeTeam( 'blue' );
    player2.die(); 
    player3.die();
    player4.die();

複製代碼

十二、裝飾器模式

​ 給對象動態的增長職責的方式稱爲裝飾者模式。裝飾者模式可以在不改變對象自身的基礎上,在程序運行期間給對象動態的添加職責。是一種即用即付的方式。

​ 裝飾者模式將一個對象嵌入到另外一個對象中,實際上至關於這個對象被另外一個對象包裝起來,造成一條包裝鏈。請求隨着這條鏈依次傳遞到全部的對象,每一個對象都有處理這條請求的機會。

一、模擬傳統面嚮對象語言的裝飾者模式

​ 分析:給對象動態增長職責的方式,並無真正地改動對象自身,而是將對象放入另外一個對象 之中,這些對象以一條鏈的方式進行引用,造成一個聚合對象。這些對象都擁有相同的接口(fire 方法),當請求達到鏈中的某個對象時,這個對象會執行自身的操做,隨後把請求轉發給鏈中的 下一個對象。

var Plane = function(){} 
//
Plane.prototype.fire = function(){     
    console.log( '發射普通子彈' ); 
} 
//接下來增長兩個裝飾類,分別是導彈和原子彈: 
var MissileDecorator = function( plane ){     
    this.plane = plane; 
}


MissileDecorator.prototype.fire = function(){     
    this.plane.fire();     
    console.log( '發射導彈' ); 
} 
//將一個對象放入另外一個對象中
var AtomDecorator = function( plane ){     
    this.plane = plane; 
} 
//給對象動態添加職責
AtomDecorator.prototype.fire = function(){     
    this.plane.fire();     
    console.log( '發射原子彈' ); 
} 
var plane = new Plane(); 
plane = new MissileDecorator( plane ); 
plane = new AtomDecorator( plane ); 
plane.fire();

複製代碼

二、裝飾函數

var a = function(){     
    alert (1); 
} 
var _a = a; 
a = function(){     
    _a();     
    alert (2); 
} 
a();

複製代碼

裝飾函數的優勢:

​ 將行爲依照職責分紅粒度更細的函數,隨後經過裝飾把它們合併到一塊兒,這有助於我 們編寫一個鬆耦合和高複用性的系統。

裝飾函數缺點:

  • 必須維護中間變量,若是函數的裝飾鏈較長,或者 須要裝飾的函數變多,這些中間變量的數量也會愈來愈多。

  • this 被劫持的問題。

    var _getElementById = document.getElementById; 
     
    document.getElementById = function( id ){     
        alert (1);// (1) 
        return _getElementById( id ); // 輸出: Uncaught TypeError: Illegal invocation 
    } 
     //document.getElementById方法的內部實現須要 使用 this 引用,this 在這個方法內預期是指向 document,而不是 window,調用_getElementById( id )方法時內部的this指向的是window。
     
    
    複製代碼

三、用AOP裝飾函數(給函數動態添加功能)

Function.prototype.before = function( beforefn ){     
    var __self = this;  // 保存原函數的引用 
    return function(){    // 返回包含了原函數和新函數的"代理"函數 
        beforefn.apply( this, arguments );  // 執行新函數,且保證 this 不被劫持,新函數接受的參數,
        //也會被原封不動地傳入原函數,新函數在原函數以前執行(前置裝飾) ,這樣就實現了動態裝飾的效果 
        return __self.apply( this, arguments );  // 執行原函數並返回原函數的執行結果,而且保證 this 不被劫持 
    } 
} 
//不污染原型的寫法
var before = function( fn, beforefn ){     
    return function(){         
        beforefn.apply( this, arguments );         
        return fn.apply( this, arguments );     
    } 
} 
Function.prototype.after = function( afterfn ){     
    var __self = this;     
    return function(){         
        var ret = __self.apply( this, arguments );         
        afterfn.apply( this, arguments );          
        return ret;     
    } 
}
//不污染原型的寫法
var after = function( fn, beforefn ){     
    return function(){         
        var ret = fn.apply( this, arguments );         
        afterfn.apply( this, arguments );          
        return ret;     
    } 
} 
//測試
var a = before(
    function () { console.log(3) },
    function () { console.log(2) }
);

a = before(a, function () { 
    console.log(1); 
}); 
a();//順序輸出1,2,3

複製代碼

三、1 AOP的應用實例

裝飾行爲依照職責分紅粒度更細的函數,隨後經過裝飾把它們合併到一塊兒,這有助於我 們編寫一個鬆耦合和高複用性的系統。

  • 數據統計上報

    分離業務代碼和數據統計代碼。

    //業務代碼
    var showLogin = function(){         
        console.log( '打開登陸浮層' );     
    } 
    var log = function(){         
        console.log( '上報標籤爲: ' + this.getAttribute( 'tag' ) );     
    }
    //數據統計代碼
    showLogin = showLogin.after( log );    // 打開登陸浮層以後上報數據 
    document.getElementById( 'button' ).onclick = showLogin; 
    
    複製代碼
  • 用AOP動態改變函數的參數

    var func = function( param ){     
        console.log( param );    // 輸出: {a: "a", b: "b"} 
    } 
    func = func.before( function( param ){     
        param.b = 'b'; 
    }); 
    //在調用func以前調用before
    func( {a: 'a'} );
    
    複製代碼
  • 插件式的表單驗證

    Function.prototype.before = function (beforefn) {
         var __self = this; return function () {
             if (beforefn.apply(this, arguments) === false) {
                 // beforefn 返回 false 的狀況直接 return,再也不執行後面的原函數 
                 return;
             }
             return __self.apply(this, arguments);
         }
     }
    //代碼驗證
    var validata = function () {
        if (username.value === '') {
            alert('用戶名不能爲空');
            return false;
        }
        if (password.value === '') {
            alert('密碼不能爲空');
            return false;
        }
    }
    //代碼提交
    var formSubmit = function () {
        var param = { username: username.value, password: password.value }
        ajax('http:// xxx.com/login', param);
    }
    //將代碼驗證和代碼提交耦合性下降
    formSubmit = formSubmit.before(validata);
    submitBtn.onclick = function () {
        formSubmit();
    }
    
    複製代碼

3.2 用AOP裝飾函數的缺點

  • 經過 Function.prototype.before 或者 Function.prototype.after 被裝 飾以後,返回的其實是一個新的函數,若是在原函數上保存了一些屬性,那麼這些屬性會丟失。由於原函數所指向的內存地址發生了變化,原函數指向了另外一個函數。

    var func = function () { alert(1); } 
    func.a = 'a';
    func = func.after(function () { alert(2); });
    alert(func.a);   // 輸出:undefined 
    
    複製代碼
  • 這種裝飾方式也疊加了函數的做用域,若是裝飾的鏈條過長,性能上也會受到一些 影響。

四、裝飾者模式和代理模式的區別

相同點: 都描述了怎樣爲對象提供必定程度上的間接引用。它們的實現部分都保留了對另外一個對象的引用(返回一個函數),而且向那個對象發送請求(調用返回的函數)。

區別: 最主要的區別在於設計目的和意圖。

  • 代理模式的目的在於當直接訪問本體或者不方便訪問本體時,爲本體提供一個替代者,代理能夠攔截一些對本體的訪問請求。或者再訪問本體以前作一些額外的事情。代理模式強調的是一種在一開始就肯定的靜態關係。代理模式一般只有一層代理-本體的引用。
  • 裝飾者模式在一開始不能肯定對象的所有功能。而裝飾者模式經常會造成一條裝飾者鏈。

1三、狀態模式

定義: 容許一個對象在其內部狀態改變時改變它的行爲,對象看起來彷佛修改了它的類。

​ 狀態模式的關鍵是區分事物的內部狀態,由於事物內部狀態的改變每每會引發事物行爲的改變。

狀態模式的關鍵是把事物的每種狀態都封裝成單獨的類,跟此類有關的行爲都被封裝在這個類的內部。

狀態模式的通用結構

class State{
    constructor(light){
        this.light=light;
    };
    buttonPress(state){
        // this.light.setState( this.light.offLightState );
        this.light.setState( state ); 
    }
}
class OffLightState extends State{
    constructor(){
        super(this);
    };
}
class WeakLightState{
    constructor(light){
        super(light);
    }
}
class StrongLightState{
    constructor(light){
        super(light);
    }
}
class SuperStrongLightState{
    constructor(light){
        super(light);
    }
}
class Light{
    constructor(){
        this.offLightState = new OffLightState( this );    // 持有狀態對象的引用 // 將對象保存爲對象的屬性。
        this.weakLightState = new WeakLightState( this );    
        this.strongLightState = new StrongLightState( this );   
        this.superStrongLightState = new SuperStrongLightState( this );  
        this.button = null; 
    };
    init(){
        this.currentState=this.offLightState;
        this.button=document.createElement("button");
        this.button.innerHTML="開關";
        this.button.onclick=()=>{
            this.currentState.buttonPress();
        }
    };
    setState(newState){
        this.currState = newState; 
    }
}
var light = new Light(); 
light.init(); 

複製代碼

狀態模式的優缺點

缺點: 會在系統中定義許多狀態類。

優勢

  • 狀態模式定義了狀態與行爲之間的關係,並將它們封裝在一個類裏。經過增長新的狀態 ,很容易增長新的狀態和轉換。
  • 避免 Context(Light類)無限膨脹,狀態切換的邏輯被分佈在狀態類中,也去掉了 Context中本來過多的條件分支。
  • 對象代替字符串來記錄當前狀態,使得狀態的切換更加一目瞭然。
  • Context中的請求動做和狀態類中封裝的行爲能夠很是容易地獨立變化而互不影響。

狀態模式和策略模式的關係

區別:策略模式中的各個策略類之間是平等又平行的,它們之間沒有任何聯繫, 因此客戶必須熟知這些策略類的做用,以便客戶能夠隨時主動切換算法。在狀態模式中,狀態 和狀態對應的行爲是早已被封裝好的,狀態之間的切換也早被規定完成,「改變行爲」這件事情 發生在狀態模式內部。

相同點: 都有一個上下文、一些策略或者狀態類,上下文把請 求委託給這些類來執行。


1四、適配器模式

var guangdongCity = { 
    shenzhen: 11, 
    guangzhou: 12, 
    zhuhai: 13 
};
var getGuangdongCity = function () {
    var guangdongCity = [
        { name: 'shenzhen', id: 11, },
        { name: 'guangzhou', id: 12, }
    ];
    return guangdongCity;
};
var render = function (fn) {
    console.log('開始渲染廣東省地圖');
    document.write(JSON.stringify(fn()));
};

var addressAdapter = function (oldAddressfn) {
    var address = {}, oldAddress = oldAddressfn();
    for (var i = 0, c; c = oldAddress[i++];) {
        address[c.name] = c.id;
    }
    return function () {
        return address;
    }
};
render(addressAdapter(getGuangdongCity));

複製代碼

適配器模式、裝飾者模式、代理模式和外觀模式的區別

相同點: 都屬於包裝模式。都是由一個對象來包裝另外一個對象。

區別:

  • 適配器模式主要是用來解決兩個接口之間不匹配的問題。不考慮這些接口是怎樣實現的,也不考慮這些接口未來怎麼變化。

  • 裝飾者模式和代理模式也不會改變原有對象的接口,但裝飾者模式的做用是爲了給對象增長功能。裝飾者模式經常造成一條長的裝飾鏈,而適配器模式一般只包裝一次。代理模式是爲了控制對對象的訪問,一般也只包裝一次。

  • 外觀模式的做用和適配器比較類似,有人把外觀模式當作一組對象的適配器,但外觀模式顯著的特色是定義了一個新的接口。

2、設計原則和編程技巧

一、第一職責原則(Single Resonsibility Principle——SRP)

​ 體現爲一個對象(方法)只作一件事情。

優勢: 下降了單個類或對象的複雜度,按照職責將對象分解爲更小的粒度,這有助於代碼的複用,同時也有助於單元測試。這樣當一個職責變動的時候,不會影響到其餘的功能。下降了代碼耦合度。

不足: 會增長編寫代碼的複雜度。將對象按照職責分解成更小粒度後,同時也增長了這些對象互相聯繫的難度。

​ 設計模式中有用到第一職責原則的有單例模式、代理模式、迭代器模式、裝飾者模式

二、最少知識原則(Least Knowledge Principle——LKP)

​ 指的是一個對象儘可能減小與其餘對象之間發生相互做用。

​ 設計模式中用到最少知識原則的是中介者模式、外觀模式、。封裝也是最少知識原則的一種體現。

三、開放封閉原則(Open Closed Principle——OCP)

​ 當須要改變一個程序的功能或者說是要給該程序增長新功能時,可使用增長代碼的方式,可是不要不容許更改程序的原代碼。

​ AOP動態裝飾函數就很好的運用到了開放閉合原則。

​ 開放閉合原則最重的就是把程序中變化的部分找出並封裝起來,將程序中不變和變化的部分隔離開來。

實行開放封閉原則的常見方法有:

  • 利用對象的多態性。

  • 放置掛鉤。在程序有可能發生變化的地方放置一個掛鉤,掛鉤的返回結果決定了程序的下一步走向

  • 使用回調函數。以把一部分易於變化的邏輯封裝在回調函數裏,而後把回調函數看成參數傳入一個穩定和封閉的函數中。當回調函數被執行的時候,程序就能夠由於回調函數的內部邏輯不一樣,而產生不一樣的結果

    設計模式中用到開放閉合原則的主要有發佈訂閱模式、模板方法模式、策略模式、代理模式、職責鏈模式

四、代碼重構

​ 模式和重構有着一種與生俱來的關係,設計模式的行爲的目標就是爲了代碼重構作準備。

代碼重構的主要手段

  • 提煉函數。

  • 合併重複的條件片斷。

  • 把條件分支語句提煉成函數。

    var isSummer = function(){     
        var date = new Date();     
        return date.getMonth() >= 6 && date.getMonth() <= 9;     
    }; 
     
    var getPrice = function( price ){     
        if ( isSummer() ){    // 夏天 
            return price * 0.8;     
        }     
        return price; 
    }; 
    
    複製代碼
  • 合理使用循環。

    var createXHR = function(){ 
        var versions= [ 'MSXML2.XMLHttp.6.0ddd', 'MSXML2.XMLHttp.3.0', 'MSXML2.XMLHttp' ];     
        for ( var i = 0, version; version = versions[ i++ ]; ){         
            try{             
                return new ActiveXObject( version );         
            }catch(e){ 
     
            }     
        } 
    }; 
     
    var xhr = createXHR(); 
    
    複製代碼
  • 提早讓函數退出代替嵌套條件分支 。

    var del = function( obj ){     
        if ( obj.isReadOnly ){    
            // 反轉 if 表達式 
            return;     
        }     
        if ( obj.isFolder ){         
            return deleteFolder( obj );     
        }     
        if ( obj.isFile ){         
            return deleteFile( obj );     
        } 
    }; 
    
    複製代碼
  • 傳遞對象參數代替過長的參數列表

    var setUserInfo = function( obj ){     
        console.log( 'id= ' + obj.id );     
        console.log( 'name= ' + obj.name );     
        console.log( 'address= ' + obj.address );     
        console.log( 'sex= ' + obj.sex );     
        console.log( 'mobile= ' + obj.mobile );     
        console.log( 'qq= ' + obj.qq ); 
    }; 
    setUserInfo({ id: 1314, name: 'sven', address: 'shenzhen', sex: 'male', mobile: '137********', qq: 377876679 }); 
    
    複製代碼
  • 儘可能減小參數數量

  • 少用三目運算符

  • 合理使用鏈式調

  • 分解大型類

  • 用return 退出多重循環

    var func = function(){     
        for ( var i = 0; i < 10; i++ ){         
            for ( var j = 0; j < 10; j++ ){             
                if ( i * j >30 ){ 
                    //避免有代碼沒有被執行
                    return print( i );            
                }         
            }     
        } 
    }; 
    
    複製代碼
相關文章
相關標籤/搜索