Javascript常用的設計模式詳解

閱讀目錄

一:理解工廠模式

   工廠模式類似於現實生活中的工廠可以產生大量相似的商品,去做同樣的事情,實現同樣的效果;這時候需要使用工廠模式。

   簡單的工廠模式可以理解爲解決多個相似的問題;這也是她的優點;比如如下代碼: 

複製代碼
function CreatePerson(name,age,sex) {
    var obj = new Object();
    obj.name = name;
    obj.age = age;
    obj.sex = sex;
    obj.sayName = function(){
        return this.name;
    }
    return obj;
}
var p1 = new CreatePerson("longen",'28','男');
var p2 = new CreatePerson("tugenhua",'27','女');
console.log(p1.name); // longen
console.log(p1.age);  // 28
console.log(p1.sex);  //
console.log(p1.sayName()); // longen

console.log(p2.name);  // tugenhua
console.log(p2.age);   // 27
console.log(p2.sex);   //
console.log(p2.sayName()); // tugenhua

// 返回都是object 無法識別對象的類型 不知道他們是哪個對象的實列
console.log(typeof p1);  // object
console.log(typeof p2);  // object
console.log(p1 instanceof Object); // true
複製代碼

如上代碼:函數CreatePerson能接受三個參數name,age,sex等參數,可以無數次調用這個函數,每次返回都會包含三個屬性和一個方法的對象。

工廠模式是爲了解決多個類似對象聲明的問題;也就是爲了解決實列化對象產生重複的問題。

優點:能解決多個相似的問題。

缺點:不能知道對象識別的問題(對象的類型不知道)

複雜的工廠模式定義是:將其成員對象的實列化推遲到子類中,子類可以重寫父類接口方法以便創建的時候指定自己的對象類型。

 父類只對創建過程中的一般性問題進行處理,這些處理會被子類繼承,子類之間是相互獨立的,具體的業務邏輯會放在子類中進行編寫。

 父類就變成了一個抽象類,但是父類可以執行子類中相同類似的方法,具體的業務邏輯需要放在子類中去實現;比如我現在開幾個自行車店,那麼每個店都有幾種型號的自行車出售。我們現在來使用工廠模式來編寫這些代碼;

父類的構造函數如下

複製代碼
// 定義自行車的構造函數
var BicycleShop = function(){};
BicycleShop.prototype = {
    constructor: BicycleShop,
    /*
    * 買自行車這個方法
    * @param {model} 自行車型號
    */
    sellBicycle: function(model){
        var bicycle = this.createBicycle(mode);
        // 執行A業務邏輯
        bicycle.A();

        // 執行B業務邏輯
        bicycle.B();

        return bicycle;
    },
    createBicycle: function(model){
        throw new Error("父類是抽象類不能直接調用,需要子類重寫該方法");
    }
};
複製代碼

上面是定義一個自行車抽象類來編寫工廠模式的實列,定義了createBicycle這個方法,但是如果直接實例化父類,調用父類中的這個createBicycle方法,會拋出一個error,因爲父類是一個抽象類,他不能被實列化,只能通過子類來實現這個方法,實現自己的業務邏輯,下面我們來定義子類,我們學會如何使用工廠模式重新編寫這個方法,首先我們需要繼承父類中的成員,然後編寫子類;如下代碼:

複製代碼
// 定義自行車的構造函數
var BicycleShop = function(name){
    this.name = name;
    this.method = function(){
        return this.name;
    }
};
BicycleShop.prototype = {
    constructor: BicycleShop,
    /*
     * 買自行車這個方法
     * @param {model} 自行車型號
    */
    sellBicycle: function(model){
            var bicycle = this.createBicycle(model);
            // 執行A業務邏輯
            bicycle.A();

            // 執行B業務邏輯
            bicycle.B();

            return bicycle;
        },
        createBicycle: function(model){
            throw new Error("父類是抽象類不能直接調用,需要子類重寫該方法");
        }
    };
    // 實現原型繼承
    function extend(Sub,Sup) {
        //Sub表示子類,Sup表示超類
        // 首先定義一個空函數
        var F = function(){};

        // 設置空函數的原型爲超類的原型
        F.prototype = Sup.prototype; 

        // 實例化空函數,並把超類原型引用傳遞給子類
        Sub.prototype = new F();
                    
        // 重置子類原型的構造器爲子類自身
        Sub.prototype.constructor = Sub;
                    
        // 在子類中保存超類的原型,避免子類與超類耦合
        Sub.sup = Sup.prototype;

        if(Sup.prototype.constructor === Object.prototype.constructor) {
            // 檢測超類原型的構造器是否爲原型自身
            Sup.prototype.constructor = Sup;
        }
    }
    var BicycleChild = function(name){
        this.name = name;
// 繼承構造函數父類中的屬性和方法
        BicycleShop.call(this,name);
    };
    // 子類繼承父類原型方法
    extend(BicycleChild,BicycleShop);
// BicycleChild 子類重寫父類的方法
BicycleChild.prototype.createBicycle = function(){
    var A = function(){
        console.log("執行A業務操作");    
    };
    var B = function(){
        console.log("執行B業務操作");
    };
    return {
        A: A,
        B: B
    }
}
var childClass = new BicycleChild("龍恩");
console.log(childClass);
複製代碼

實例化子類,然後打印出該實例如下截圖所示:

console.log(childClass.name);  // 龍恩

// 下面是實例化後 執行父類中的sellBicycle這個方法後會依次調用父類中的A

// B方法;A方法和B方法依次在子類中去編寫具體的業務邏輯。

childClass.sellBicycle("mode"); // 打印出  執行A業務操作和執行B業務操作

上面只是"龍恩"自行車這麼一個型號的,如果需要生成其他型號的自行車的話,可以編寫其他子類,工廠模式最重要的優點是:可以實現一些相同的方法,這些相同的方法我們可以放在父類中編寫代碼,那麼需要實現具體的業務邏輯,那麼可以放在子類中重寫該父類的方法,去實現自己的業務邏輯;使用專業術語來講的話有2點:第一:弱化對象間的耦合,防止代碼的重複。在一個方法中進行類的實例化,可以消除重複性的代碼。第二:重複性的代碼可以放在父類去編寫,子類繼承於父類的所有成員屬性和方法,子類只專注於實現自己的業務邏輯。

二:理解單體模式

單體模式提供了一種將代碼組織爲一個邏輯單元的手段,這個邏輯單元中的代碼可以通過單一變量進行訪問。

單體模式的優點是:

  1. 可以用來劃分命名空間,減少全局變量的數量。
  2. 使用單體模式可以使代碼組織的更爲一致,使代碼容易閱讀和維護。
  3. 可以被實例化,且實例化一次。

什麼是單體模式?單體模式是一個用來劃分命名空間並將一批屬性和方法組織在一起的對象,如果它可以被實例化,那麼它只能被實例化一次。

但是並非所有的對象字面量都是單體,比如說模擬數組或容納數據的話,那麼它就不是單體,但是如果是組織一批相關的屬性和方法在一起的話,那麼它有可能是單體模式,所以這需要看開發者編寫代碼的意圖;

下面我們來看看定義一個對象字面量(結構類似於單體模式)的基本結構如下:

複製代碼
// 對象字面量
var Singleton = {
    attr1: 1,
    attr2: 2,
    method1: function(){
        return this.attr1;
    },
    method2: function(){
        return this.attr2;
    }
};
複製代碼

如上面只是簡單的字面量結構,上面的所有成員變量都是通過Singleton來訪問的,但是它並不是單體模式;因爲單體模式還有一個更重要的特點,就是可以僅被實例化一次,上面的只是不能被實例化的一個類,因此不是單體模式;對象字面量是用來創建單體模式的方法之一;

使用單體模式的結構如下demo

我們明白的是單體模式如果有實例化的話,那麼只實例化一次,要實現一個單體模式的話,我們無非就是使用一個變量來標識該類是否被實例化,如果未被實例化的話,那麼我們可以實例化一次,否則的話,直接返回已經被實例化的對象。

如下代碼是單體模式的基本結構:

複製代碼
// 單體模式
var Singleton = function(name){
    this.name = name;
    this.instance = null;
};
Singleton.prototype.getName = function(){
    return this.name;
}
// 獲取實例對象
function getInstance(name) {
    if(!this.instance) {
        this.instance = new Singleton(name);
    }
    return this.instance;
}
// 測試單體模式的實例
var a = getInstance("aa");
var b = getInstance("bb");
複製代碼

// 因爲單體模式是隻實例化一次,所以下面的實例是相等的

console.log(a === b); // true

由於單體模式只實例化一次,因此第一次調用,返回的是a實例對象,當我們繼續調用的時候,b的實例就是a的實例,因此下面都是打印的是aa

console.log(a.getName());// aa

console.log(b.getName());// aa

上面的封裝單體模式也可以改成如下結構寫法:

複製代碼
// 單體模式
var Singleton = function(name){
    this.name = name;
};
Singleton.prototype.getName = function(){
    return this.name;
}
// 獲取實例對象
var getInstance = (function() {
    var instance = null;
    return function(name) {
        if(!instance) {
            instance = new Singleton(name);
        }
        return instance;
    }
})();
// 測試單體模式的實例
var a = getInstance("aa");
var b = getInstance("bb");
複製代碼

// 因爲單體模式是隻實例化一次,所以下面的實例是相等的

console.log(a === b); // true

console.log(a.getName());// aa

console.log(b.getName());// aa

理解使用代理實現單列模式的好處
    比如我現在頁面上需要創建一個div的元素,那麼我們肯定需要有一個創建div的函數,而現在我只需要這個函數只負責創建div元素,其他的它不想管,也就是想實現單一職責原則,就好比淘寶的kissy一樣,一開始的時候他們定義kissy只做一件事,並且把這件事做好,具體的單體模式中的實例化類的事情交給代理函數去處理,這樣做的好處是具體的業務邏輯分開了,代理只管代理的業務邏輯,在這裏代理的作用是實例化對象,並且只實例化一次創建div代碼只管創建div,其他的不管;如下代碼:

複製代碼
// 單體模式
var CreateDiv = function(html) {
    this.html = html;
    this.init();
}
CreateDiv.prototype.init = function(){
    var div = document.createElement("div");
    div.innerHTML = this.html;
    document.body.appendChild(div);
};
// 代理實現單體模式
var ProxyMode = (function(){
    var instance;
    return function(html) {
        if(!instance) {
            instance = new CreateDiv("我來測試下");
        }
        return instance;
    } 
})();
var a = new ProxyMode("aaa");
var b = new ProxyMode("bbb");
console.log(a===b);// true
複製代碼

理解使用單體模式來實現彈窗的基本原理

下面我們繼續來使用單體模式來實現一個彈窗的demo;我們先不討論使用單體模式來實現,我們想下我們平時是怎麼編寫代碼來實現彈窗效果的; 比如我們有一個彈窗,默認的情況下肯定是隱藏的,當我點擊的時候,它需要顯示出來;如下編寫代碼:

複製代碼
// 實現彈窗
var createWindow = function(){
    var div = document.createElement("div");
    div.innerHTML = "我是彈窗內容";
    div.style.display = 'none';
    document.body.appendChild('div');
    return div;
};
document.getElementById("Id").onclick = function(){
    // 點擊後先創建一個div元素
    var win = createWindow();
    win.style.display = "block";
}
複製代碼

如上的代碼;大家可以看看,有明顯的缺點,比如我點擊一個元素需要創建一個div,我點擊第二個元素又會創建一次div,我們頻繁的點擊某某元素,他們會頻繁的創建div的元素,雖然當我們點擊關閉的時候可以移除彈出代碼,但是呢我們頻繁的創建和刪除並不好,特別對於性能會有很大的影響,對DOM頻繁的操作會引起重繪等,從而影響性能;因此這是非常不好的習慣;我們現在可以使用單體模式來實現彈窗效果,我們只實例化一次就可以了;如下代碼:

複製代碼
// 實現單體模式彈窗
var createWindow = (function(){
    var div;
    return function(){
        if(!div) {
            div = document.createElement("div");
            div.innerHTML = "我是彈窗內容";
            div.style.display = 'none';
            document.body.appendChild(div);
        }
        return div;
    }
})();
document.getElementById("Id").onclick = function(){
    // 點擊後先創建一個div元素
    var win = createWindow();
    win.style.display = "block";
}
複製代碼

理解編寫通用的單體模式

上面的彈窗的代碼雖然完成了使用單體模式創建彈窗效果,但是代碼並不通用,比如上面是完成彈窗的代碼,假如我們以後需要在頁面中一個iframe呢?我們是不是需要重新寫一套創建iframe的代碼呢?比如如下創建iframe:

複製代碼
var createIframe = (function(){
    var iframe;
    return function(){
        if(!iframe) {
            iframe = document.createElement("iframe");
            iframe.style.display = 'none';
            document.body.appendChild(iframe);
        }
        return iframe;
    };
})();
複製代碼

我們看到如上代碼,創建div的代碼和創建iframe代碼很類似,我們現在可以考慮把通用的代碼分離出來,使代碼變成完全抽象,我們現在可以編寫一套代碼封裝在getInstance函數內,如下代碼:

複製代碼
var getInstance = function(fn) {
    var result;
    return function(){
        return result || (result = fn.call(this,arguments));
    }
};
複製代碼

如上代碼:我們使用一個參數fn傳遞進去,如果有result這個實例的話,直接返回,否則的話,當前的getInstance函數調用fn這個函數,是this指針指向與這個fn這個函數;之後返回被保存在result裏面;現在我們可以傳遞一個函數進去,不管他是創建div也好,還是創建iframe也好,總之如果是這種的話,都可以使用getInstance來獲取他們的實例對象;

如下測試創建iframe和創建div的代碼如下:

複製代碼
// 創建div
var createWindow = function(){
    var div = document.createElement("div");
    div.innerHTML = "我是彈窗內容";
    div.style.display = 'none';
    document.body.appendChild(div);
    return div;
};
// 創建iframe
var createIframe = function(){
    var iframe = document.createElement("iframe");
    document.body.appendChild(iframe);
    return iframe;
};
// 獲取實例的封裝代碼
var getInstance = function(fn) {
    var result;
    return function(){
        return result || (result = fn.call(this,arguments));
    }
};
// 測試創建div
var createSingleDiv = getInstance(createWindow);
document.getElementById("Id").onclick = function(){
    var win = createSingleDiv();
    win.style.display = "block";
};
// 測試創建iframe
var createSingleIframe = getInstance(createIframe);
document.getElementById("Id").onclick = function(){
    var win = createSingleIframe();
    win.src = "http://cnblogs.com";
};
複製代碼

三:理解模塊模式

我們通過單體模式理解了是以對象字面量的方式來創建單體模式的;比如如下的對象字面量的方式代碼如下:

複製代碼
var singleMode = {
    name: value,
    method: function(){
                
    }
};
複製代碼

模塊模式的思路是爲單體模式添加私有變量和私有方法能夠減少全局變量的使用;如下就是一個模塊模式的代碼結構:

複製代碼
var singleMode = (function(){
    // 創建私有變量
    var privateNum = 112;
    // 創建私有函數
    function privateFunc(){
        // 實現自己的業務邏輯代碼
    }
    // 返回一個對象包含公有方法和屬性
    return {
        publicMethod1: publicMethod1,
        publicMethod2: publicMethod1
    };
})();
複製代碼

   模塊模式使用了一個返回對象的匿名函數。在這個匿名函數內部,先定義了私有變量和函數,供內部函數使用,然後將一個對象字面量作爲函數的值返回,返回的對象字面量中只包含可以公開的屬性和方法。這樣的話,可以提供外部使用該方法;由於該返回對象中的公有方法是在匿名函數內部定義的,因此它可以訪問內部的私有變量和函數。

我們什麼時候使用模塊模式?

如果我們必須創建一個對象並以某些數據進行初始化,同時還要公開一些能夠訪問這些私有數據的方法,那麼我們這個時候就可以使用模塊模式了。

理解增強的模塊模式

增強的模塊模式的使用場合是:適合那些單列必須是某種類型的實例,同時還必須添加某些屬性或方法對其加以增強的情況。比如如下代碼:

複製代碼
function CustomType() {
    this.name = "tugenhua";
};
CustomType.prototype.getName = function(){
    return this.name;
}
var application = (function(){
    // 定義私有
    var privateA = "aa";
    // 定義私有函數
    function A(){};

    // 實例化一個對象後,返回該實例,然後爲該實例增加一些公有屬性和方法
    var object = new CustomType();

    // 添加公有屬性
    object.A = "aa";
    // 添加公有方法
    object.B = function(){
        return privateA;
    }
    // 返回該對象
    return object;
})();
複製代碼

下面我們來打印下application該對象;如下:

console.log(application);

繼續打印該公有屬性和方法如下:

console.log(application.A);// aa

console.log(application.B()); // aa

console.log(application.name); // tugenhua

console.log(application.getName());// tugenhua

四:理解代理模式

     代理是一個對象,它可以用來控制對本體對象的訪問,它與本體對象實現了同樣的接口,代理對象會把所有的調用方法傳遞給本體對象的;代理模式最基本的形式是對訪問進行控制,而本體對象則負責執行所分派的那個對象的函數或者類,簡單的來講本地對象注重的去執行頁面上的代碼,代理則控制本地對象何時被實例化,何時被使用;我們在上面的單體模式中使用過一些代理模式,就是使用代理模式實現單體模式的實例化,其他的事情就交給本體對象去處理;

代理的優點:

  1. 代理對象可以代替本體被實例化,並使其可以被遠程訪問;
  2. 它還可以把本體實例化推遲到真正需要的時候;對於實例化比較費時的本體對象,或者因爲尺寸比較大以至於不用時不適於保存在內存中的本體,我們可以推遲實例化該對象;

我們先來理解代理對象代替本體對象被實例化的列子;比如現在京東ceo想送給奶茶妹一個禮物,但是呢假如該ceo不好意思送,或者由於工作忙沒有時間送,那麼這個時候他就想委託他的經紀人去做這件事,於是我們可以使用代理模式來編寫如下代碼:

複製代碼
// 先申明一個奶茶妹對象
var TeaAndMilkGirl = function(name) {
    this.name = name;
};
// 這是京東ceo先生
var Ceo = function(girl) {
    this.girl = girl;
    // 送結婚禮物 給奶茶妹
    this.sendMarriageRing = function(ring) {
        console.log("Hi " + this.girl.name + ", ceo送你一個禮物:" + ring);
    }
};
// 京東ceo的經紀人是代理,來代替送
var ProxyObj = function(girl){
    this.girl = girl;
    // 經紀人代理送禮物給奶茶妹
    this.sendGift = function(gift) {
        // 代理模式負責本體對象實例化
        (new Ceo(this.girl)).sendMarriageRing(gift);
    }
};
// 初始化
var proxy = new ProxyObj(new TeaAndMilkGirl("奶茶妹"));
proxy.sendGift("結婚戒"); // Hi 奶茶妹, ceo送你一個禮物:結婚戒
複製代碼

代碼如上的基本結構,TeaAndMilkGirl 是一個被送的對象(這裏是奶茶妹);Ceo 是送禮物的對象,他保存了奶茶妹這個屬性,及有一個自己的特權方法sendMarriageRing 就是送禮物給奶茶妹這麼一個方法;然後呢他是想通過他的經紀人去把這件事完成,於是需要創建一個經濟人的代理模式,名字叫ProxyObj ;他的主要做的事情是,把ceo交給他的禮物送給ceo的情人,因此該對象同樣需要保存ceo情人的對象作爲自己的屬性,同時也需要一個特權方法sendGift ,該方法是送禮物,因此在該方法內可以實例化本體對象,這裏的本體對象是ceo送花這件事情,因此需要實例化該本體對象後及調用本體對象的方法(sendMarriageRing).

最後我們初始化是需要代理對象ProxyObj;調用ProxyObj 對象的送花這個方法(sendGift)即可;

對於我們提到的優點,第二點的話,我們下面可以來理解下虛擬代理,虛擬代理用於控制對那種創建開銷很大的本體訪問,它會把本體的實例化推遲到有方法被調用的時候;比如說現在有一個對象的實例化很慢的話,不能在網頁加載的時候立即完成,我們可以爲其創建一個虛擬代理,讓他把該對象的實例推遲到需要的時候。

理解使用虛擬代理實現圖片的預加載

在網頁開發中,圖片的預加載是一種比較常用的技術,如果直接給img標籤節點設置src屬性的話,如果圖片比較大的話,或者網速相對比較慢的話,那麼在圖片未加載完之前,圖片會有一段時間是空白的場景,這樣對於用戶體驗來講並不好,那麼這個時候我們可以在圖片未加載完之前我們可以使用一個loading加載圖片來作爲一個佔位符,來提示用戶該圖片正在加載,等圖片加載完後我們可以對該圖片直接進行賦值即可;下面我們先不用代理模式來實現圖片的預加載的情況下代碼如下:

第一種方案:不使用代理的預加載圖片函數如下

複製代碼
// 不使用代理的預加載圖片函數如下
var myImage = (function(){
    var imgNode = document.createElement("img");
    document.body.appendChild(imgNode);
    var img = new Image();
    img.onload = function(){
        imgNode.src = this.src;
    };
    return {
        setSrc: function(src) {
            imgNode.src = "http://img.lanrentuku.com/img/allimg/1212/5-121204193Q9-50.gif";
            img.src = src;
        }
    }
})();
// 調用方式
myImage.setSrc("https://img.alicdn.com/tps/i4/TB1b_neLXXXXXcoXFXXc8PZ9XXX-130-200.png");
複製代碼

如上代碼是不使用代理模式來實現的代碼;

第二種方案:使用代理模式來編寫預加載圖片的代碼如下:

複製代碼
var myImage = (function(){
    var imgNode = document.createElement("img");
    document.body.appendChild(imgNode);
    return {
        setSrc: function(src) {
            imgNode.src = src;
        }
    }
})();
// 代理模式
var ProxyImage = (function(){
    var img = new Image();
    img.onload = function(){
        myImage.setSrc(this.src);
    };
    return {
        setSrc: function(src) {
                         myImage.setSrc("http://img.lanrentuku.com/img/allimg/1212/5-121204193Q9-50.gif");
        img.src = src;
        }
    }
})();
// 調用方式
ProxyImage.setSrc("https://img.alicdn.com/tps/i4/TB1b_neLXXXXXcoXFXXc8PZ9XXX-130-200.png");
複製代碼

第一種方案是使用一般的編碼方式實現圖片的預加載技術,首先創建imgNode元素,然後調用myImage.setSrc該方法的時候,先給圖片一個預加載圖片,當圖片加載完的時候,再給img元素賦值,第二種方案是使用代理模式來實現的,myImage 函數只負責創建img元素,代理函數ProxyImage 負責給圖片設置loading圖片,當圖片真正加載完後的話,調用myImage中的myImage.setSrc方法設置圖片的路徑;他們之間的優缺點如下:

  1. 第一種方案一般的方法代碼的耦合性太高,一個函數內負責做了幾件事情,比如創建img元素,和實現給未加載圖片完成之前設置loading加載狀態等多項事情,未滿足面向對象設計原則中單一職責原則;並且當某個時候不需要代理的時候,需要從myImage 函數內把代碼刪掉,這樣代碼耦合性太高。
  2. 第二種方案使用代理模式,其中myImage 函數只負責做一件事,創建img元素加入到頁面中,其中的加載loading圖片交給代理函數ProxyImage 去做,當圖片加載成功後,代理函數ProxyImage 會通知及執行myImage 函數的方法,同時當以後不需要代理對象的話,我們直接可以調用本體對象的方法即可;

從上面代理模式我們可以看到,代理模式和本體對象中有相同的方法setSrc,這樣設置的話有如下2個優點:

  1. 用戶可以放心地請求代理,他們只關心是否能得到想要的結果。假如我門不需要代理對象的話,直接可以換成本體對象調用該方法即可。
  2. 在任何使用本體對象的地方都可以替換成使用代理。

當然如果代理對象和本體對象都返回一個匿名函數的話,那麼也可以認爲他們也具有一直的接口;比如如下代碼:

複製代碼
var myImage = (function(){
    var imgNode = document.createElement("img");
    document.body.appendChild(imgNode);
    return function(src){
        imgNode.src = src; 
    }
})();
// 代理模式
var ProxyImage = (function(){
    var img = new Image();
    img.onload = function(){
        myImage(this.src);
    };
    return function(src) {
                myImage("http://img.lanrentuku.com/img/allimg/1212/5-121204193Q9-50.gif");
        img.src = src;
    }
})();
// 調用方式
ProxyImage("https://img.alicdn.com/tps/i4/TB1b_neLXXXXXcoXFXXc8PZ9XXX-130-200.png");
複製代碼

虛擬代理合並http請求的理解:

   比如在做後端系統中,有表格數據,每一條數據前面有複選框按鈕,當點擊複選框按鈕時候,需要獲取該id後需要傳遞給給服務器發送ajax請求,服務器端需要記錄這條數據,去請求,如果我們每當點擊一下向服務器發送一個http請求的話,對於服務器來說壓力比較大,網絡請求比較頻繁,但是如果現在該系統的實時數據不是很高的話,我們可以通過一個代理函數收集一段時間內(比如說2-3秒)的所有id,一次性發ajax請求給服務器,相對來說網絡請求降低了, 服務器壓力減少了;

複製代碼
// 首先html結構如下:
<p>
    <label>選擇框</label>
    <input type="checkbox" class="j-input" data-id="1"/>
</p>
<p>
    <label>選擇框</label>
    <input type="checkbox" class="j-input" data-id = "2"/>
</p>
<p>
    <label>選擇框</label>
    <input type="checkbox" class="j-input" data-id="3"/>
</p>
<p>
    <label>選擇框</label>
    <input type="checkbox" class="j-input" data-id = "4"/>
</p>
複製代碼

一般的情況下 JS如下編寫

複製代碼
<script>
    var checkboxs = document.getElementsByClassName("j-input");
    for(var i = 0,ilen = checkboxs.length; i < ilen; i+=1) {
        (function(i){
            checkboxs[i].onclick = function(){
                if(this.checked) {
                    var id = this.getAttribute("data-id");
                    // 如下是ajax請求
                }
            }
        })(i);
    }
</script>
複製代碼

下面我們通過虛擬代理的方式,延遲2秒,在2秒後獲取所有被選中的複選框的按鈕id,一次性給服務器發請求。

  通過點擊頁面的複選框,選中的時候增加一個屬性isflag,沒有選中的時候刪除該屬性isflag,然後延遲個2秒,在2秒後重新判斷頁面上所有複選框中有isflag的屬性上的id,存入數組,然後代理函數調用本體函數的方法,把延遲2秒後的所有id一次性發給本體方法,本體方法可以獲取所有的id,可以向服務器端發送ajax請求,這樣的話,服務器的請求壓力相對來說減少了。

代碼如下:

複製代碼
// 本體函數
var mainFunc = function(ids) {
    console.log(ids); // 即可打印被選中的所有的id
    // 再把所有的id一次性發ajax請求給服務器端
};
// 代理函數 通過代理函數獲取所有的id 傳給本體函數去執行
var proxyFunc = (function(){
    var cache = [],  // 保存一段時間內的id
        timer = null; // 定時器
    return function(checkboxs) {
        // 判斷如果定時器有的話,不進行覆蓋操作
        if(timer) {
            return;
        }
        timer = setTimeout(function(){
            // 在2秒內獲取所有被選中的id,通過屬性isflag判斷是否被選中
            for(var i = 0,ilen = checkboxs.length; i < ilen; i++) {
                if(checkboxs[i].hasAttribute("isflag")) {
                    var id = checkboxs[i].getAttribute("data-id");
                    cache[cache.length] = id;
                }
            }
            mainFunc(cache.join(',')); // 2秒後需要給本體函數傳遞所有的id
            // 清空定時器
            clearTimeout(timer);
            timer = null;
            cache = [];
        },2000);
    }
})();
var checkboxs = document.getElementsByClassName("j-input");
for(var i = 0,ilen = checkboxs.length; i < ilen; i+=1) {
    (function(i){
        checkboxs[i].onclick = function(){
            if(this.checked) {
                // 給當前增加一個屬性
                this.setAttribute("isflag",1);
            }else {
                this.removeAttribute('isflag');
            }
            // 調用代理函數
            proxyFunc(checkboxs);
        }
    })(i);
}
複製代碼

理解緩存代理:

   緩存代理的含義就是對第一次運行時候進行緩存,當再一次運行相同的時候,直接從緩存裏面取,這樣做的好處是避免重複一次運算功能,如果運算非常複雜的話,對性能很耗費,那麼使用緩存對象可以提高性能;我們可以先來理解一個簡單的緩存列子,就是網上常見的加法和乘法的運算。代碼如下:

複製代碼
// 計算乘法
var mult = function(){
    var a = 1;
    for(var i = 0,ilen = arguments.length; i < ilen; i+=1) {
        a = a*arguments[i];
    }
    return a;
};
// 計算加法
var plus = function(){
    var a = 0;
    for(var i = 0,ilen = arguments.length; i < ilen; i+=1) {
        a += arguments[i];
    }
    return a;
}
// 代理函數
var proxyFunc = function(fn) {
    var cache = {};  // 緩存對象
    return function(){
        var args = Array.prototype.join.call(arguments,',');
        if(args in cache) {
            return cache[args];   // 使用緩存代理
        }
        return cache[args] = fn.apply(this,arguments);
    }
};
var proxyMult = proxyFunc(mult);
console.log(proxyMult(1,2,3,4)); // 24
console.log(proxyMult(1,2,3,4)); // 緩存取 24

var proxyPlus = proxyFunc(plus);
console.log(proxyPlus(1,2,3,4));  // 10
console.log(proxyPlus(1,2,3,4));  // 緩存取 10
複製代碼

五:理解職責鏈模式

優點是:消除請求的發送者與接收者之間的耦合。

    職責連是由多個不同的對象組成的,發送者是發送請求的對象,而接收者則是鏈中那些接收這種請求並且對其進行處理或傳遞的對象。請求本身有時候也可以是一個對象,它封裝了和操作有關的所有數據,基本實現流程如下:

1. 發送者知道鏈中的第一個接收者,它向這個接收者發送該請求。

2. 每一個接收者都對請求進行分析,然後要麼處理它,要麼它往下傳遞。

3. 每一個接收者知道其他的對象只有一個,即它在鏈中的下家(successor)。

4. 如果沒有任何接收者處理請求,那麼請求會從鏈中離開。

   我們可以理解職責鏈模式是處理請求組成的一條鏈,請求在這些對象之間依次傳遞,直到遇到一個可以處理它的對象,我們把這些對象稱爲鏈中的節點。比如對象A給對象B發請求,如果B對象不處理,它就會把請求交給C,如果C對象不處理的話,它就會把請求交給D,依次類推,直到有一個對象能處理該請求爲止,當然沒有任何對象處理該請求的話,那麼請求就會從鏈中離開。

   比如常見的一些外包公司接到一個項目,那麼接到項目有可能是公司的負責項目的人或者經理級別的人,經理接到項目後自己不開發,直接把它交到項目經理來開發,項目經理自己肯定不樂意自己動手開發哦,它就把項目交給下面的碼農來做,所以碼農來處理它,如果碼農也不處理的話,那麼這個項目可能會直接掛掉了,但是最後完成後,外包公司它並不知道這些項目中的那一部分具體有哪些人開發的,它並不知道,也並不關心的,它關心的是這個項目已交給外包公司已經開發完成了且沒有任何bug就可以了;所以職責鏈模式的優點就在這裏:

消除請求的發送者(需要外包項目的公司)與接收者(外包公司)之間的耦合。

下面列舉個列子來說明職責鏈的好處:

天貓每年雙11都會做抽獎活動的,比如阿里巴巴想提高大家使用支付寶來支付的話,每一位用戶充值500元到支付寶的話,那麼可以100%中獎100元紅包,

充值200元到支付寶的話,那麼可以100%中獎20元的紅包,當然如果不充值的話,也可以抽獎,但是概率非常低,基本上是抽不到的,當然也有可能抽到的。

我們下面可以分析下代碼中的幾個字段值需要來判斷:

1. orderType(充值類型),如果值爲1的話,說明是充值500元的用戶,如果爲2的話,說明是充值200元的用戶,如果是3的話,說明是沒有充值的用戶。

2. isPay(是否已經成功充值了): 如果該值爲true的話,說明已經成功充值了,否則的話 說明沒有充值成功;就當作普通用戶來購買。

3. count(表示數量);普通用戶抽獎,如果數量有的話,就可以拿到優惠卷,否則的話,不能拿到優惠卷。

複製代碼
// 我們一般寫代碼如下處理操作
var order =  function(orderType,isPay,count) {
    if(orderType == 1) {  // 用戶充值500元到支付寶去
        if(isPay == true) { // 如果充值成功的話,100%中獎
            console.log("親愛的用戶,您中獎了100元紅包了");
        }else {
            // 充值失敗,就當作普通用戶來處理中獎信息
            if(count > 0) {
                console.log("親愛的用戶,您已抽到10元優惠卷");
            }else {
                console.log("親愛的用戶,請再接再厲哦");
            }
        }
    }else if(orderType == 2) {  // 用戶充值200元到支付寶去
        if(isPay == true) {     // 如果充值成功的話,100%中獎
            console.log("親愛的用戶,您中獎了20元紅包了");
        }else {
            // 充值失敗,就當作普通用戶來處理中獎信息
            if(count > 0) {
                console.log("親愛的用戶,您已抽到10元優惠卷");
            }else {
                console.log("親愛的用戶,請再接再厲哦");
            }
        }
    }else if(orderType == 3) {
        // 普通用戶來處理中獎信息
        if(count > 0) {
            console.log("親愛的用戶,您已抽到10元優惠卷");
        }else {
            console.log("親愛的用戶,請再接再厲哦");
        }
    }
};
複製代碼

上面的代碼雖然可以實現需求,但是代碼不容易擴展且難以閱讀,假如以後我想一兩個條件,我想充值300元成功的話,可以中獎150元紅包,那麼這時候又要改動裏面的代碼,這樣業務邏輯與代碼耦合性相對比較高,一不小心就改錯了代碼;這時候我們試着使用職責鏈模式來依次傳遞對象來實現;

如下代碼:

複製代碼
function order500(orderType,isPay,count){
    if(orderType == 1 && isPay == true)    {
        console.log("親愛的用戶,您中獎了100元紅包了");
    }else {
        // 自己不處理,傳遞給下一個對象order200去處理
        order200(orderType,isPay,count);
    }
};
function order200(orderType,isPay,count) {
    if(orderType == 2 && isPay == true) {
        console.log("親愛的用戶,您中獎了20元紅包了");
    }else {
        // 自己不處理,傳遞給下一個對象普通用戶去處理
        orderNormal(orderType,isPay,count);
    }
};
function orderNormal(orderType,isPay,count){
    // 普通用戶來處理中獎信息
    if(count > 0) {
        console.log("親愛的用戶,您已抽到10元優惠卷");
    }else {
        console.log("親愛的用戶,請再接再厲哦");
    }
}
複製代碼

如上代碼我們分別使用了三個函數order500,order200,orderNormal來分別處理自己的業務邏輯,如果目前的自己函數不能處理的事情,我們傳遞給下面的函數去處理,依次類推,直到有一個函數能處理他,否則的話,該職責鏈模式直接從鏈中離開,告訴不能處理,拋出錯誤提示,上面的代碼雖然可以當作職責鏈模式,但是我們看上面的代碼可以看到order500函數內依賴了order200這樣的函數,這樣就必須有這個函數,也違反了面向對象中的 開放-封閉原則。下面我們繼續來理解編寫 靈活可拆分的職責鏈節點。

複製代碼
function order500(orderType,isPay,count){
    if(orderType == 1 && isPay == true)    {
        console.log("親愛的用戶,您中獎了100元紅包了");
    }else {
        //我不知道下一個節點是誰,反正把請求往後面傳遞
        return "nextSuccessor";
    }
};
function order200(orderType,isPay,count) {
    if(orderType == 2 && isPay == true) {
        console.log("親愛的用戶,您中獎了20元紅包了");
    }else {
        //我不知道下一個節點是誰,反正把請求往後面傳遞
        return "nextSuccessor";
    }
};
function orderNormal(orderType,isPay,count){
    // 普通用戶來處理中獎信息
    if(count > 0) {
        console.log("親愛的用戶,您已抽到10元優惠卷");
    }else {
        console.log("親愛的用戶,請再接再厲哦");
    }
}
// 下面需要編寫職責鏈模式的封裝構造函數方法
var Chain = function(fn){
    this.fn = fn;
    this.successor = null;
};
Chain.prototype.setNextSuccessor = function(successor){
    return this.successor = successor;
}
// 把請求往下傳遞
Chain.prototype.passRequest = function(){
    var ret = this.fn.apply(this,arguments);
    if(ret === 'nextSuccessor') {
        return this.successor && this.successor.passRequest.apply(this.successor,arguments);
    }
    return ret;
}
//現在我們把3個函數分別包裝成職責鏈節點:
var chainOrder500 = new Chain(order500);
var chainOrder200 = new Chain(order200);
var chainOrderNormal = new Chain(orderNormal);

// 然後指定節點在職責鏈中的順序
chainOrder500.setNextSuccessor(chainOrder200);
chainOrder200.setNextSuccessor(chainOrderNormal);

//最後把請求傳遞給第一個節點:
chainOrder500.passRequest(1,true,500);  // 親愛的用戶,您中獎了100元紅包了
chainOrder500.passRequest(2,true,500);  // 親愛的用戶,您中獎了20元紅包了
chainOrder500.passRequest(3,true,500);  // 親愛的用戶,您已抽到10元優惠卷 
chainOrder500.passRequest(1,false,0);   // 親愛的用戶,請再接再厲哦
複製代碼

如上代碼;分別編寫order500,order200,orderNormal三個函數,在函數內分別處理自己的業務邏輯,如果自己的函數不能處理的話,就返回字符串nextSuccessor 往後面傳遞,然後封裝Chain這個構造函數,傳遞一個fn這個對象實列進來,且有自己的一個屬性successor,原型上有2個方法 setNextSuccessor 和 passRequest;setNextSuccessor 這個方法是指定節點在職責鏈中的順序的,把相對應的方法保存到this.successor這個屬性上,chainOrder500.setNextSuccessor(chainOrder200);chainOrder200.setNextSuccessor(chainOrderNormal);指定鏈中的順序,因此this.successor引用了order200這個方法和orderNormal這個方法,因此第一次chainOrder500.passRequest(1,true,500)調用的話,調用order500這個方法,直接輸出,第二次調用chainOrder500.passRequest(2,true,500);這個方法從鏈中首節點order500開始不符合,就返回successor字符串,然後this.successor && this.successor.passRequest.apply(this.successor,arguments);就執行這句代碼;上面我們說過this.successor這個屬性引用了2個方法 分別爲order200和orderNormal,因此調用order200該方法,所以就返回了值,依次類推都是這個原理。那如果以後我們想充值300元的紅包的話,我們可以編寫order300這個函數,然後實列一下鏈chain包裝起來,指定一下職責鏈中的順序即可,裏面的業務邏輯不需要做任何處理;

理解異步的職責鏈

上面的只是同步職責鏈,我們讓每個節點函數同步返回一個特定的值”nextSuccessor”,來表示是否把請求傳遞給下一個節點,在我們開發中會經常碰到ajax異步請求,請求成功後,需要做某某事情,那麼這時候如果我們再套用上面的同步請求的話,就不生效了,下面我們來理解下使用異步的職責鏈來解決這個問題;我們給Chain類再增加一個原型方法Chain.prototype.next,表示手動傳遞請求給職責鏈中的一下個節點。

如下代碼:

複製代碼
function Fn1() {
    console.log(1);
    return "nextSuccessor";
}
function Fn2() {
    console.log(2);
    var self = this;
    setTimeout(function(){
        self.next();
    },1000);
}
function Fn3() {
    console.log(3);
}
// 下面需要編寫職責鏈模式的封裝構造函數方法
var Chain = function(fn){
    this.fn = fn;
    this.successor = null;
};
Chain.prototype.setNextSuccessor = function(successor){
    return this.successor = successor;
}
// 把請求往下傳遞
Chain.prototype.passRequest = function(){
    var ret = this.fn.apply(this,arguments);
    if(ret === 'nextSuccessor') {
        return this.successor && this.successor.passRequest.apply(this.successor,arguments);
    }
    return ret;
}
Chain.prototype.next = function(){
    return this.successor && this.successor.passRequest.apply(this.successor,arguments);
}
//現在我們把3個函數分別包裝成職責鏈節點:
var chainFn1 = new Chain(Fn1);
var chainFn2 = new Chain(Fn2);
var chainFn3 = new Chain(Fn3);

// 然後指定節點在職責鏈中的順序
chainFn1.setNextSuccessor(chainFn2);
chainFn2.setNextSuccessor(chainFn3);

chainFn1.passRequest();  // 打印出1,2 過1秒後 會打印出3
複製代碼

調用函數 chainFn1.passRequest();後,會先執行發送者Fn1這個函數 打印出1,然後返回字符串 nextSuccessor;

 接着就執行return this.successor && this.successor.passRequest.apply(this.successor,arguments);這個函數到Fn2,打印2,接着裏面有一個setTimeout定時器異步函數,需要把請求給職責鏈中的下一個節點,因此過一秒後會打印出3;

職責鏈模式的優點是:

 1. 解耦了請求發送者和N個接收者之間的複雜關係,不需要知道鏈中那個節點能處理你的請求,所以你

    只需要把請求傳遞到第一個節點即可。

 2. 鏈中的節點對象可以靈活地拆分重組,增加或刪除一個節點,或者改變節點的位置都是很簡單的事情。

 3. 我們還可以手動指定節點的起始位置,並不是說非得要從其實節點開始傳遞的.

 缺點:職責鏈模式中多了一點節點對象,可能在某一次請求過程中,大部分節點沒有起到實質性作用,他們的作用只是讓

 請求傳遞下去,從性能方面考慮,避免過長的職責鏈提高性能。

六:命令模式的理解

 命令模式中的命令指的是一個執行某些特定事情的指令。

   命令模式使用的場景有:有時候需要向某些對象發送請求,但是並不知道請求的接收者是誰,也不知道請求的操作是什麼,此時希望用一種松耦合的方式來設計程序代碼;使得請求發送者和請求接受者消除彼此代碼中的耦合關係。

我們先來列舉生活中的一個列子來說明下命令模式:比如我們經常會在天貓上購買東西,然後下訂單,下單後我就想收到貨,並且希望貨物是真的,對於用戶來講它並關心下單後賣家怎麼發貨,當然賣家發貨也有時間的,比如24小時內發貨等,用戶更不關心快遞是給誰派送,當然有的人會關心是什麼快遞送貨的; 對於用戶來說,只要在規定的時間內發貨,且一般能在相當的時間內收到貨就可以,當然命令模式也有撤銷命令和重做命令,比如我們下單後,我突然不想買了,我在發貨之前可以取消訂單,也可以重新下單(也就是重做命令);比如我的衣服尺碼拍錯了,我取消該訂單,重新拍一個大碼的。

1. 命令模式的列子

   記得我以前剛做前端的那會兒,也就是剛畢業進的第一家公司,進的是做外包項目的公司,該公司一般外包淘寶活動頁面及騰訊的遊戲頁面,我們那會兒應該叫切頁面的前端,負責做一些html和css的工作,所以那會兒做騰訊的遊戲頁面,經常會幫他們做靜態頁面,比如在頁面放幾個按鈕,我們只是按照設計稿幫騰訊遊戲哪方面的把樣式弄好,比如說頁面上的按鈕等事情,比如說具體說明的按鈕要怎麼操作,點擊按鈕後會發生什麼事情,我們並不知道,我們不知道他們的業務是什麼,當然我們知道的肯定會有點擊事件,具體要處理什麼業務我們並不知道,這裏我們就可以使用命令模式來處理了:點擊按鈕之後,必須向某些負責具體行爲的對象發送請求,這些對象就是請求的接收者。但是目前我們並不知道接收者是什麼對象,也不知道接受者究竟會做什麼事情,這時候我們可以使用命令模式來消除發送者與接收者的代碼耦合關係。

我們先使用傳統的面向對象模式來設計代碼:

假設html結構如下:
<button id="button1">刷新菜單目錄</button>
<button id="button2">增加子菜單</button>
<button id="button3">刪除子菜單</button>

JS代碼如下:

複製代碼
var b1 = document.getElementById("button1"),
     b2 = document.getElementById("button2"),
     b3 = document.getElementById("button3");
     
 // 定義setCommand 函數,該函數負責往按鈕上面安裝命令。點擊按鈕後會執行command對象的execute()方法。
 var setCommand = function(button,command){
    button.onclick = function(){
        command.execute();
    }
 };
 // 下面我們自己來定義各個對象來完成自己的業務操作
 var MenuBar = {
    refersh: function(){
        alert("刷新菜單目錄");
    }
 };
 var SubMenu = {
    add: function(){
        alert("增加子菜單");
    },
    del: function(){
        alert("刪除子菜單");
    }
 };
 // 下面是編寫命令類
 var RefreshMenuBarCommand = function(receiver){
    this.receiver = receiver;
 };
 RefreshMenuBarCommand.prototype.execute = function(){
    this.receiver.refersh();
 }
 // 增加命令操作
 var AddSubMenuCommand = function(receiver) {
    this.receiver = receiver;
 };
 AddSubMenuCommand.prototype.execute = function() {
    this.receiver.add();
 }
 // 刪除命令操作
 var DelSubMenuCommand = function(receiver) {
    this.receiver = receiver;
 };
 DelSubMenuCommand.prototype.execute = function(){
    this.receiver.del();
 }
 // 最後把命令接收者傳入到command對象中,並且把command對象安裝到button上面
 var refershBtn = new RefreshMenuBarCommand(MenuBar);
 var addBtn = new AddSubMenuCommand(SubMenu);
 var delBtn = new DelSubMenuCommand(SubMenu);
 
 setCommand(b1,refershBtn);
 setCommand(b2,addBtn);
 setCommand(b3,delBtn);
複製代碼

從上面的命令類代碼我們可以看到,任何一個操作都有一個execute這個方法來執行操作;上面的代碼是使用傳統的面向對象編程來實現命令模式的,命令模式過程式的請求調用封裝在command對象的execute方法裏。我們有沒有發現上面的編寫代碼有點繁瑣呢,我們可以使用javascript中的回調函數來做這些事情的,在面向對象中,命令模式的接收者被當成command對象的屬性保存起來,同時約定執行命令的操作調用command.execute方法,但是如果我們使用回調函數的話,那麼接收者被封閉在回調函數產生的環境中,執行操作將會更加簡單,僅僅執行回調函數即可,下面我們來看看代碼如下:

代碼如下:

複製代碼
var setCommand = function(button,func) {
    button.onclick = function(){
        func();
    }
 }; 
 var MenuBar = {
    refersh: function(){
        alert("刷新菜單界面");
    }
 };
 var SubMenu = {
    add: function(){
        alert("增加菜單");
    }
 };
 // 刷新菜單
 var RefreshMenuBarCommand = function(receiver) {
    return function(){
        receiver.refersh();    
    };
 };
 // 增加菜單
 var AddSubMenuCommand = function(receiver) {
    return function(){
        receiver.add();    
    };
 };
 var refershMenuBarCommand = RefreshMenuBarCommand(MenuBar);
 // 增加菜單
 var addSubMenuCommand = AddSubMenuCommand(SubMenu);
 setCommand(b1,refershMenuBarCommand);
 
 setCommand(b2,addSubMenuCommand);
複製代碼

我們還可以如下使用javascript回調函數如下編碼:

複製代碼
// 如下代碼上的四個按鈕 點擊事件
var b1 = document.getElementById("button1"),
    b2 = document.getElementById("button2"),
    b3 = document.getElementById("button3"),
    b4 = document.getElementById("button4");
/*
 bindEnv函數負責往按鈕上面安裝點擊命令。點擊按鈕後,會調用
 函數
 */
var bindEnv = function(button,func) {
    button.onclick = function(){
        func();
    }
};
// 現在我們來編寫具體處理業務邏輯代碼
var Todo1 = {
    test1: function(){
        alert("我是來做第一個測試的");
    }    
};
// 實現業務中的增刪改操作
var Menu = {
    add: function(){
        alert("我是來處理一些增加操作的");
    },
    del: function(){
        alert("我是來處理一些刪除操作的");
    },
    update: function(){
        alert("我是來處理一些更新操作的");
    }
};
// 調用函數
bindEnv(b1,Todo1.test1);
// 增加按鈕
bindEnv(b2,Menu.add);
// 刪除按鈕
bindEnv(b3,Menu.del);
// 更改按鈕
bindEnv(b4,Menu.update);
複製代碼

2. 理解宏命令:

   宏命令是一組命令的集合,通過執行宏命令的方式,可以一次執行一批命令。

其實類似把頁面的所有函數方法放在一個數組裏面去,然後遍歷這個數組,依次

執行該方法的。

代碼如下:

複製代碼
var command1 = {
    execute: function(){
        console.log(1);
    }
}; 
var command2 = {
    execute: function(){
        console.log(2);
    }
};
var command3 = {
    execute: function(){
        console.log(3);
    }
};
// 定義宏命令,command.add方法把子命令添加進宏命令對象,
// 當調用宏命令對象的execute方法時,會迭代這一組命令對象,
// 並且依次執行他們的execute方法。
var command = function(){
    return {
        commandsList: [],
        add: function(command){
            this.commandsList.push(command);
        },
        execute: function(){
            for(var i = 0,commands = this.commandsList.length; i < commands; i+=1) {
                this.commandsList[i].execute();
            }
        }
    }
};
// 初始化宏命令
var c = command();
c.add(command1);
c.add(command2);
c.add(command3);
c.execute();  // 1,2,3
複製代碼

七:模板方法模式

    模板方法模式由二部分組成,第一部分是抽象父類,第二部分是具體實現的子類,一般的情況下是抽象父類封裝了子類的算法框架,包括實現一些公共方法及封裝子類中所有方法的執行順序,子類可以繼承這個父類,並且可以在子類中重寫父類的方法,從而實現自己的業務邏輯。

比如說我們要實現一個JS功能,比如表單驗證等js,那麼如果我們沒有使用上一章講的使用javascript中的策略模式來解決表單驗證封裝代碼,而是自己寫的臨時表單驗證功能,肯定是沒有進行任何封裝的,那麼這個時候我們是針對兩個值是否相等給用戶彈出一個提示,如果再另外一個頁面也有一個表單驗證,他們判斷的方式及業務邏輯基本相同的,只是比較的參數不同而已,我們是不是又要考慮寫一個表單驗證代碼呢?那麼現在我們可以考慮使用模板方法模式來解決這個問題;公用的方法提取出來,不同的方法由具體的子類是實現。這樣設計代碼也可擴展性更強,代碼更優等優點~

我們不急着寫代碼,我們可以先來看一個列子,比如最近經常在qq羣裏面有很多前端招聘的信息,自己也接到很多公司或者獵頭問我是否需要找工作等電話,當然我現在是沒有打算找工作的,因爲現在有更多的業餘時間可以處理自己的事情,所以也覺得蠻不錯的~ 我們先來看看招聘中面試這個流程;面試流程對於很多大型公司,比如BAT,面試過程其實很類似;因此我們可以總結面試過程中如下:

1. 筆試:(不同的公司有不同的筆試題目)。

2. 技術面試(一般情況下分爲二輪):第一輪面試你的有可能是你未來直接主管或者未來同事問你前端的一些專業方面的技能及以前做過的項目,在項目中遇到哪些問題及當時是如何解決問題的,還有根據你的簡歷上的基本信息來交流的,比如說你簡歷說精通JS,那麼人家肯定得問哦~ 第二輪面試一般都是公司的牛人或者架構師來問的,比如問你計算機基本原理,或者問一些數據結構與算法等信息;第二輪面試可能會更深入的去了解你這個人的技術。

3. HR和總監或者總經理面試;那麼這一輪的話,HR可能會問下你一些個人基本信息等情況,及問下你今後有什麼打算的個人規劃什麼的,總監或者總經理可能會問下你對他們的網站及產品有了解過沒有?及現在他們的產品有什麼問題,有沒有更好的建議或者如何改善的地方等信息;

4. 最後就是HR和你談薪資及一般幾個工作日可以得到通知,拿到offer(當然不符合的肯定是沒有通知的哦);及自己有沒有需要了解公司的情況等等信息;

一般的面試過程都是如上四點下來的,對於不同的公司都差不多的流程的,當然有些公司可能沒有上面的詳細流程的,我這邊這邊講一般的情況下,好了,這邊就不扯了,這邊也不是講如何面試的哦,這邊只是通過這個列子讓我們更加的理解javascript中模板方法模式;所以我們現在回到正題上來;

我們先來分析下上面的流程;我們可以總結如下:

首先我們看一下百度的面試;因此我們可以先定義一個構造函數。

var BaiDuInterview = function(){};

那麼下面就有百度面試的流程哦~

1. 筆試

那麼我們可以封裝一個筆試的方法,代碼如下:

// baidu 筆試

BaiDuInterview.prototype.writtenTest = function(){

    console.log("我終於看到百度的筆試題了~");

};

2. 技術面試:

// 技術面試

BaiDuInterview.prototype.technicalInterview = function(){

    console.log("我是百度的技術負責人");

}; 

 3.  HR和總監或者總經理面試,我們可以稱之爲leader面試;代碼如下:

 // 領導面試

BaiDuInterview.prototype.leader = function(){

    console.log("百度leader來面試了");

};

4. HR談期望的薪資待遇及HR會告訴你什麼時候會有通知,因此我們這邊可以稱之爲這個方法爲 是否拿到offer(當然不符合要求肯定是沒有通知的哦)

// 等通知

BaiDuInterview.prototype.waitNotice = function(){

    console.log("百度的人力資源太不給力了,到現在都不給我通知");

};

如上看到代碼的基本結構,但是我們還需要一個初始化方法;代碼如下:

// 代碼初始化

BaiDuInterview.prototype.init = function(){

    this.writtenTest();

    this.technicalInterview();

    this.leader();

    this.waitNotice();

};

var baiDuInterview = new BaiDuInterview();

baiDuInterview.init();

綜合所述:所有的代碼如下:

var BaiDuInterview = function(){};

 

// baidu 筆試

BaiDuInterview.prototype.writtenTest = function(){

    console.log("我終於看到百度的題目筆試題了~");

};

// 技術面試

BaiDuInterview.prototype.technicalInterview = function(){

    console.log("我是百度的技術負責人");

}; 

// 領導面試

BaiDuInterview.prototype.leader = function(){

    console.log("百度leader來面試了");

};

// 等通知

BaiDuInterview.prototype.waitNotice = function(){

    console.log("百度的人力資源太不給力了,到現在都不給我通知");

};

// 代碼初始化

BaiDuInterview.prototype.init = function(){

    this.writtenTest();

    this.technicalInterview();

    this.leader();

    this.waitNotice();

};

var baiDuInterview = new BaiDuInterview();

baiDuInterview.init();

 

上面我們可以看到百度面試的基本流程如上面的代碼,那麼阿里和騰訊的也和上面的代碼類似(這裏就不一一貼一樣的代碼哦),因此我們可以把公用代碼提取出來;我們首先定義一個類,叫面試Interview

那麼代碼改成如下:

var Interview = function(){};

1. 筆試:

我不管你是百度的筆試還是阿里或者騰訊的筆試題,我這邊統稱爲筆試(WrittenTest),那麼你們公司有不同的筆試題,都交給子類去具體實現,父類方法不管具體如何實現,筆試題具體是什麼樣的 我都不管。代碼變爲如下:

// 筆試

Interview.prototype.writtenTest = function(){

    console.log("我終於看到筆試題了~");

};

2. 技術面試,技術面試原理也一樣,這裏就不多說,直接貼代碼:

// 技術面試

Interview.prototype.technicalInterview = function(){

    console.log("我是技術負責人負責技術面試");

}; 

3. 領導面試

// 領導面試

Interview.prototype.leader = function(){

    console.log("leader來面試了");

};

4. 等通知

// 等通知

Interview.prototype.waitNotice = function(){

    console.log("人力資源太不給力了,到現在都不給我通知");

};

代碼初始化方法如下:

// 代碼初始化

Interview.prototype.init = function(){

    this.writtenTest();

    this.technicalInterview();

    this.leader();

    this.waitNotice();

};

二:創建子類

現在我們來創建一個百度的子類來繼承上面的父類;代碼如下:

var BaiDuInterview = function(){};

BaiDuInterview.prototype = new Interview();

現在我們可以在子類BaiDuInterview 重寫父類Interview中的方法;代碼如下:

// 子類重寫方法 實現自己的業務邏輯

BaiDuInterview.prototype.writtenTest = function(){

    console.log("我終於看到百度的筆試題了");

}

BaiDuInterview.prototype.technicalInterview = function(){

    console.log("我是百度的技術負責人,想面試找我");

}

BaiDuInterview.prototype.leader = function(){

    console.log("我是百度的leader,不想加班的或者業績提不上去的給我滾蛋");

}

BaiDuInterview.prototype.waitNotice = function(){

    console.log("百度的人力資源太不給力了,我等的花兒都謝了!!");

}

var baiDuInterview = new BaiDuInterview();

baiDuInterview.init();

如上看到,我們直接調用子類baiDuInterview.init()方法,由於我們子類baiDuInterview沒有init方法,但是它繼承了父類,所以會到父類中查找對應的init方法;所以會迎着原型鏈到父類中查找;對於其他子類,比如阿里類代碼也是一樣的,這裏就不多介紹了,對於父類這個方法 Interview.prototype.init() 是模板方法,因爲他封裝了子類中算法框架,它作爲一個算法的模板,指導子類以什麼樣的順序去執行代碼。

三: Javascript中的模板模式使用場景

雖然在java中也有子類實現父類的接口,但是我認爲javascript中可以和java中不同的,java中可能父類就是一個空的類,子類去實現這個父類的接口,在javascript中我認爲完全把公用的代碼寫在父函數內,如果將來業務邏輯需要更改的話,或者說添加新的業務邏輯,我們完全可以使用子類去重寫這個父類,這樣的話代碼可擴展性強,更容易維護。由於本人不是專業java的,所以描述java中的知識點有誤的話,請理解~~

八:理解javascript中的策略模式

1. 理解javascript中的策略模式

策略模式的定義是:定義一系列的算法,把它們一個個封裝起來,並且使它們可以相互替換。

使用策略模式的優點如下:

優點:1. 策略模式利用組合,委託等技術和思想,有效的避免很多if條件語句。

      2. 策略模式提供了開放-封閉原則,使代碼更容易理解和擴展。

      3. 策略模式中的代碼可以複用。

一:使用策略模式計算獎金;

下面的demo是我在書上看到的,但是沒有關係,我們只是來理解下策略模式的使用而已,我們可以使用策略模式來計算獎金問題;

比如公司的年終獎是根據員工的工資和績效來考覈的,績效爲A的人,年終獎爲工資的4倍,績效爲B的人,年終獎爲工資的3倍,績效爲C的人,年終獎爲工資的2倍;現在我們使用一般的編碼方式會如下這樣編寫代碼:

複製代碼
var calculateBouns = function(salary,level) {
    if(level === 'A') {
        return salary * 4;
    }
    if(level === 'B') {
        return salary * 3;
    }
    if(level === 'C') {
        return salary * 2;
    }
};
// 調用如下:
console.log(calculateBouns(4000,'A')); // 16000
console.log(calculateBouns(2500,'B')); // 7500
複製代碼

第一個參數爲薪資,第二個參數爲等級;

代碼缺點如下:

calculateBouns 函數包含了很多if-else語句。

calculateBouns 函數缺乏彈性,假如還有D等級的話,那麼我們需要在calculateBouns 函數內添加判斷等級D的if語句;

算法複用性差,如果在其他的地方也有類似這樣的算法的話,但是規則不一樣,我們這些代碼不能通用。

2. 使用組合函數重構代碼

組合函數是把各種算法封裝到一個個的小函數裏面,比如等級A的話,封裝一個小函數,等級爲B的話,也封裝一個小函數,以此類推;如下代碼:

複製代碼
var performanceA = function(salary) {
    return salary * 4;
};
var performanceB = function(salary) {
    return salary * 3;
};
        
var performanceC = function(salary) {
    return salary * 2
};
var calculateBouns = function(level,salary) {
    if(level === 'A') {
        return performanceA(salary);
    }
    if(level === 'B') {
        return performanceB(salary);
    }
    if(level === 'C') {
        return performanceC(salary);
    }
};
// 調用如下
console.log(calculateBouns('A',4500)); // 18000
複製代碼

代碼看起來有點改善,但是還是有如下缺點:

calculateBouns 函數有可能會越來越大,比如增加D等級的時候,而且缺乏彈性。

3. 使用策略模式重構代碼

策略模式指的是 定義一系列的算法,把它們一個個封裝起來,將不變的部分和變化的部分隔開,實際就是將算法的使用和實現分離出來;算法的使用方式是不變的,都是根據某個算法取得計算後的獎金數,而算法的實現是根據績效對應不同的績效規則;

一個基於策略模式的程序至少由2部分組成,第一個部分是一組策略類,策略類封裝了具體的算法,並負責具體的計算過程。第二個部分是環境類Context,該Context接收客戶端的請求,隨後把請求委託給某一個策略類。我們先使用傳統面向對象來實現;

如下代碼:

複製代碼
var performanceA = function(){};
performanceA.prototype.calculate = function(salary) {
    return salary * 4;
};      
var performanceB = function(){};
performanceB.prototype.calculate = function(salary) {
    return salary * 3;
};
var performanceC = function(){};
performanceC.prototype.calculate = function(salary) {
    return salary * 2;
};
// 獎金類
var Bouns = function(){
    this.salary = null;    // 原始工資
    this.levelObj = null;  // 績效等級對應的策略對象
};
Bouns.prototype.setSalary = function(salary) {
    this.salary = salary;  // 保存員工的原始工資
};
Bouns.prototype.setlevelObj = function(levelObj){
    this.levelObj = levelObj;  // 設置員工績效等級對應的策略對象
};
// 取得獎金數
Bouns.prototype.getBouns = function(){
    // 把計算獎金的操作委託給對應的策略對象
    return this.levelObj.calculate(this.salary);
};
var bouns = new Bouns();
bouns.setSalary(10000);
bouns.setlevelObj(new performanceA()); // 設置策略對象
console.log(bouns.getBouns());  // 40000
       
bouns.setlevelObj(new performanceB()); // 設置策略對象
console.log(bouns.getBouns());  // 30000
複製代碼

如上代碼使用策略模式重構代碼,可以看到代碼職責更新分明,代碼變得更加清晰。

4. Javascript版本的策略模式

複製代碼
//代碼如下:
var obj = {
        "A": function(salary) {
            return salary * 4;
        },
        "B" : function(salary) {
            return salary * 3;
        },
        "C" : function(salary) {
            return salary * 2;
        } 
};
var calculateBouns =function(level,salary) {
    return obj[level](salary);
};
console.log(calculateBouns('A',10000)); // 40000
複製代碼

可以看到代碼更加簡單明瞭;

策略模式指的是定義一系列的算法,並且把它們封裝起來,但是策略模式不僅僅只封裝算法,我們還可以對用來封裝一系列的業務規則,只要這些業務規則目標一致,我們就可以使用策略模式來封裝它們;

表單效驗

比如我們經常來進行表單驗證,比如註冊登錄對話框,我們登錄之前要進行驗證操作:比如有以下幾條邏輯:

用戶名不能爲空

密碼長度不能小於6位。

手機號碼必須符合格式。

比如HTML代碼如下:

複製代碼
<form action = "http://www.baidu.com" id="registerForm" method = "post">
        <p>
            <label>請輸入用戶名:</label>
            <input type="text" name="userName"/>
        </p>
        <p>
            <label>請輸入密碼:</label>
            <input type="text" name="password"/>
        </p>
        <p>
            <label>請輸入手機號碼:</label>
            <input type="text" name="phoneNumber"/>
        </p>
</form>
複製代碼

我們正常的編寫表單驗證代碼如下:

複製代碼
var registerForm = document.getElementById("registerForm");
registerForm.onsubmit = function(){
    if(registerForm.userName.value === '') {
        alert('用戶名不能爲空');
        return;
    }
    if(registerForm.password.value.length < 6) {
        alert("密碼的長度不能小於6位");
        return;
    }
    if(!/(^1[3|5|8][0-9]{9}$)/.test(registerForm.phoneNumber.value)) {
        alert("手機號碼格式不正確");
        return;
    }
}
複製代碼

但是這樣編寫代碼有如下缺點:

1.registerForm.onsubmit 函數比較大,代碼中包含了很多if語句;

2.registerForm.onsubmit 函數缺乏彈性,如果增加了一種新的效驗規則,或者想把密碼的長度效驗從6改成8,我們必須改registerForm.onsubmit 函數內部的代碼。違反了開放-封閉原則。

3. 算法的複用性差,如果在程序中增加了另外一個表單,這個表單也需要進行一些類似的效驗,那麼我們可能又需要複製代碼了;

下面我們可以使用策略模式來重構表單效驗;

第一步我們先來封裝策略對象;如下代碼:

複製代碼
var strategy = {
    isNotEmpty: function(value,errorMsg) {
        if(value === '') {
            return errorMsg;
        }
    },
    // 限制最小長度
    minLength: function(value,length,errorMsg) {
        if(value.length < length) {
            return errorMsg;
        }
    },
    // 手機號碼格式
    mobileFormat: function(value,errorMsg) {
        if(!/(^1[3|5|8][0-9]{9}$)/.test(value)) {
            return errorMsg;
        }
    } 
};
複製代碼

接下來我們準備實現Validator類,Validator類在這裏作爲Context,負責接收用戶的請求並委託給strategy 對象,如下代碼:

複製代碼
var Validator = function(){
    this.cache = [];  // 保存效驗規則
};
Validator.prototype.add = function(dom,rule,errorMsg) {
    var str = rule.split(":");
    this.cache.push(function(){
        // str 返回的是 minLength:6 
        var strategy = str.shift();
        str.unshift(dom.value); // 把input的value添加進參數列表
        str.push(errorMsg);  // 把errorMsg添加進參數列表
        return strategys[strategy].apply(dom,str);
    });
};
Validator.prototype.start = function(){
    for(var i = 0, validatorFunc; validatorFunc = this.cache[i++]; ) {
        var msg = validatorFunc(); // 開始效驗 並取得效驗後的返回信息
        if(msg) {
            return msg;
        }
    }
};
複製代碼

Validator類在這裏作爲Context,負責接收用戶的請求並委託給strategys對象。上面的代碼中,我們先創建一個Validator對象,然後通過validator.add方法往validator對象中添加一些效驗規則,validator.add方法接收3個參數,如下代碼:

validator.add(registerForm.password,'minLength:6','密碼長度不能小於6位');

registerForm.password 爲效驗的input輸入框dom節點;

minLength:6: 是以一個冒號隔開的字符串,冒號前面的minLength代表客戶挑選的strategys對象,冒號後面的數字6表示在效驗過程中所必須驗證的參數,minLength:6的意思是效驗 registerForm.password 這個文本輸入框的value最小長度爲6位;如果字符串中不包含冒號,說明效驗過程中不需要額外的效驗信息;

第三個參數是當效驗未通過時返回的錯誤信息;

當我們往validator對象裏添加完一系列的效驗規則之後,會調用validator.start()方法來啓動效驗。如果validator.start()返回了一個errorMsg字符串作爲返回值,說明該次效驗沒有通過,此時需要registerForm.onsubmit方法返回false來阻止表單提交。下面我們來看看初始化代碼如下:

複製代碼
var validateFunc = function(){
    var validator = new Validator(); // 創建一個Validator對象
    /* 添加一些效驗規則 */
    validator.add(registerForm.userName,'isNotEmpty','用戶名不能爲空');
    validator.add(registerForm.password,'minLength:6','密碼長度不能小於6位');
    validator.add(registerForm.userName,'mobileFormat','手機號碼格式不正確');

    var errorMsg = validator.start(); // 獲得效驗結果
    return errorMsg; // 返回效驗結果
};
var registerForm = document.getElementById("registerForm");
registerForm.onsubmit = function(){
    var errorMsg = validateFunc();
    if(errorMsg){
        alert(errorMsg);
        return false;
    }
}
複製代碼

下面是所有的代碼如下:

複製代碼
var strategys = {
    isNotEmpty: function(value,errorMsg) {
        if(value === '') {
            return errorMsg;
        }
    },
    // 限制最小長度
    minLength: function(value,length,errorMsg) {
        if(value.length < length) {
            return errorMsg;
        }
    },
    // 手機號碼格式
    mobileFormat: function(value,errorMsg) {
        if(!/(^1[3|5|8][0-9]{9}$)/.test(value)) {
            return errorMsg;
        }
    } 
};
var Validator = function(){
    this.cache = [];  // 保存效驗規則
};
Validator.prototype.add = function(dom,rule,errorMsg) {
    var str = rule.split(":");
    this.cache.push(function(){
        // str 返回的是 minLength:6 
        var strategy = str.shift();
        str.unshift(dom.value); // 把input的value添加進參數列表
        str.push(errorMsg);  // 把errorMsg添加進參數列表
        return strategys[strategy].apply(dom,str);
    });
};
Validator.prototype.start = function(){
    for(var i = 0, validatorFunc; validatorFunc = this.cache[i++]; ) {
        var msg = validatorFunc(); // 開始效驗 並取得效驗後的返回信息
        if(msg) {
            return msg;
        }
    }
};

var validateFunc = function(){
    var validator = new Validator(); // 創建一個Validator對象
    /* 添加一些效驗規則 */
    validator.add(registerForm.userName,'isNotEmpty','用戶名不能爲空');
    validator.add(registerForm.password,'minLength:6','密碼長度不能小於6位');
    validator.add(registerForm.userName,'mobileFormat','手機號碼格式不正確');

    var errorMsg = validator.start(); // 獲得效驗結果
    return errorMsg; // 返回效驗結果
};
var registerForm = document.getElementById("registerForm");
registerForm.onsubmit = function(){
    var errorMsg = validateFunc();
    if(errorMsg){
        alert(errorMsg);
        return false;
    }
};
複製代碼

如上使用策略模式來編寫表單驗證代碼可以看到好處了,我們通過add配置的方式就完成了一個表單的效驗;這樣的話,那麼代碼可以當做一個組件來使用,並且可以隨時調用,在修改表單驗證規則的時候,也非常方便,通過傳遞參數即可調用;

給某個文本輸入框添加多種效驗規則,上面的代碼我們可以看到,我們只是給輸入框只能對應一種效驗規則,比如上面的我們只能效驗輸入框是否爲空,validator.add(registerForm.userName,'isNotEmpty','用戶名不能爲空');但是如果我們既要效驗輸入框是否爲空,還要效驗輸入框的長度不要小於10位的話,那麼我們期望需要像如下傳遞參數:

validator.add(registerForm.userName,[{strategy:’isNotEmpty’,errorMsg:’用戶名不能爲空’},{strategy: 'minLength:6',errorMsg:'用戶名長度不能小於6位'}])

我們可以編寫代碼如下:

複製代碼
// 策略對象
var strategys = {
    isNotEmpty: function(value,errorMsg) {
        if(value === '') {
            return errorMsg;
        }
    },
    // 限制最小長度
    minLength: function(value,length,errorMsg) {
        if(value.length < length) {
            return errorMsg;
        }
    },
    // 手機號碼格式
    mobileFormat: function(value,errorMsg) {
        if(!/(^1[3|5|8][0-9]{9}$)/.test(value)) {
            return errorMsg;
        }
    } 
};
var Validator = function(){
    this.cache = [];  // 保存效驗規則
};
Validator.prototype.add = function(dom,rules) {
    var self = this;
    for(var i = 0, rule; rule = rules[i++]; ){
        (function(rule){
            var strategyAry = rule.strategy.split(":");
            var errorMsg = rule.errorMsg;
            self.cache.push(function(){
                var strategy = strategyAry.shift();
                strategyAry.unshift(dom.value);
                strategyAry.push(errorMsg);
                return strategys[strategy].apply(dom,strategyAry);
            });
        })(rule);
    }
};
Validator.prototype.start = function(){
    for(var i = 0, validatorFunc; validatorFunc = this.cache[i++]; ) {
    var msg = validatorFunc(); // 開始效驗 並取得效驗後的返回信息
    if(msg) {
        return msg;
    }
    }
};
// 代碼調用
var registerForm = document.getElementById("registerForm");
var validateFunc = function(){
    var validator = new Validator(); // 創建一個Validator對象
    /* 添加一些效驗規則 */
    validator.add(registerForm.userName,[
        {strategy: 'isNotEmpty',errorMsg:'用戶名不能爲空'},
        {strategy: 'minLength:6',errorMsg:'用戶名長度不能小於6位'}
    ]);
    validator.add(registerForm.password,[
        {strategy: 'minLength:6',errorMsg:'密碼長度不能小於6位'},
    ]);
    validator.add(registerForm.phoneNumber,[
        {strategy: 'mobileFormat',errorMsg:'手機號格式不正確'},
    ]);
    var errorMsg = validator.start(); // 獲得效驗結果
    return errorMsg; // 返回效驗結果
};
// 點擊確定提交
registerForm.onsubmit = function(){
    var errorMsg = validateFunc();
    if(errorMsg){
        alert(errorMsg);
        return false;
    }
}
複製代碼

注意:如上代碼都是按照書上來做的,都是看到書的代碼,最主要我們理解策略模式實現,比如上面的表單驗證功能是這樣封裝的代碼,我們平時使用jquery插件表單驗證代碼原來是這樣封裝的,爲此我們以後也可以使用這種方式來封裝表單等學習;

九:Javascript中理解發布--訂閱模式

1. 發佈訂閱模式介紹

   發佈---訂閱模式又叫觀察者模式,它定義了對象間的一種一對多的關係,讓多個觀察者對象同時監聽某一個主題對象,當一個對象發生改變時,所有依賴於它的對象都將得到通知。

  現實生活中的發佈-訂閱模式;

比如小紅最近在淘寶網上看上一雙鞋子,但是呢 聯繫到賣家後,才發現這雙鞋賣光了,但是小紅對這雙鞋又非常喜歡,所以呢聯繫賣家,問賣傢什麼時候有貨,賣家告訴她,要等一個星期後纔有貨,賣家告訴小紅,要是你喜歡的話,你可以收藏我們的店鋪,等有貨的時候再通知你,所以小紅收藏了此店鋪,但與此同時,小明,小花等也喜歡這雙鞋,也收藏了該店鋪;等來貨的時候就依次會通知他們;

在上面的故事中,可以看出是一個典型的發佈訂閱模式,賣家是屬於發佈者,小紅,小明等屬於訂閱者,訂閱該店鋪,賣家作爲發佈者,當鞋子到了的時候,會依次通知小明,小紅等,依次使用旺旺等工具給他們發佈消息;

發佈訂閱模式的優點:

  1. 支持簡單的廣播通信,當對象狀態發生改變時,會自動通知已經訂閱過的對象。

比如上面的列子,小明,小紅不需要天天逛淘寶網看鞋子到了沒有,在合適的時間點,發佈者(賣家)來貨了的時候,會通知該訂閱者(小紅,小明等人)。

  2. 發佈者與訂閱者耦合性降低,發佈者只管發佈一條消息出去,它不關心這條消息如何被訂閱者使用,同時,訂閱者只監聽發佈者的事件名,只要發佈者的事件名不變,它不管發佈者如何改變;同理賣家(發佈者)它只需要將鞋子來貨的這件事告訴訂閱者(買家),他不管買家到底買還是不買,還是買其他賣家的。只要鞋子到貨了就通知訂閱者即可。

 對於第一點,我們日常工作中也經常使用到,比如我們的ajax請求,請求有成功(success)和失敗(error)的回調函數,我們可以訂閱ajax的success和error事件。我們並不關心對象在異步運行的狀態,我們只關心success的時候或者error的時候我們要做點我們自己的事情就可以了~

發佈訂閱模式的缺點:

  創建訂閱者需要消耗一定的時間和內存。

  雖然可以弱化對象之間的聯繫,如果過度使用的話,反而使代碼不好理解及代碼不好維護等等。

2. 如何實現發佈--訂閱模式?

   1. 首先要想好誰是發佈者(比如上面的賣家)。

   2. 然後給發佈者添加一個緩存列表,用於存放回調函數來通知訂閱者(比如上面的買家收藏了賣家的店鋪,賣家通過收藏了該店鋪的一個列表名單)。

   3. 最後就是發佈消息,發佈者遍歷這個緩存列表,依次觸發裏面存放的訂閱者回調函數。

我們還可以在回調函數裏面添加一點參數,比如鞋子的顏色,鞋子尺碼等信息;

我們先來實現下簡單的發佈-訂閱模式;代碼如下:

複製代碼
var shoeObj = {}; // 定義發佈者
shoeObj.list = []; // 緩存列表 存放訂閱者回調函數
        
// 增加訂閱者
shoeObj.listen = function(fn) {
    shoeObj.list.push(fn);  // 訂閱消息添加到緩存列表
}

// 發佈消息
shoeObj.trigger = function(){
    for(var i = 0,fn; fn = this.list[i++];) {
        fn.apply(this,arguments); 
    }
}
// 小紅訂閱如下消息
shoeObj.listen(function(color,size){
    console.log("顏色是:"+color);
    console.log("尺碼是:"+size);  
});

// 小花訂閱如下消息
shoeObj.listen(function(color,size){
    console.log("再次打印顏色是:"+color);
    console.log("再次打印尺碼是:"+size); 
});
shoeObj.trigger("紅色",40);
shoeObj.trigger("黑色",42);
複製代碼

運行結果如下:

打印如上截圖,我們看到訂閱者接收到發佈者的每個消息,但是呢,對於小紅來說,她只想接收顏色爲紅色的消息,不想接收顏色爲黑色的消息,爲此我們需要對代碼進行如下改造下,我們可以先增加一個key,使訂閱者只訂閱自己感興趣的消息。代碼如下:

複製代碼
var shoeObj = {}; // 定義發佈者
shoeObj.list = []; // 緩存列表 存放訂閱者回調函數
        
// 增加訂閱者
shoeObj.listen = function(key,fn) {
    if(!this.list[key]) {
        // 如果還沒有訂閱過此類消息,給該類消息創建一個緩存列表
        this.list[key] = []; 
    }
    this.list[key].push(fn);  // 訂閱消息添加到緩存列表
}

// 發佈消息
shoeObj.trigger = function(){
    var key = Array.prototype.shift.call(arguments); // 取出消息類型名稱
    var fns = this.list[key];  // 取出該消息對應的回調函數的集合

    // 如果沒有訂閱過該消息的話,則返回
    if(!fns || fns.length === 0) {
        return;
    }
    for(var i = 0,fn; fn = fns[i++]; ) {
        fn.apply(this,arguments); // arguments 是發佈消息時附送的參數
    }
};

// 小紅訂閱如下消息
shoeObj.listen('red',function(size){
    console.log("尺碼是:"+size);  
});

// 小花訂閱如下消息
shoeObj.listen('block',function(size){
    console.log("再次打印尺碼是:"+size); 
});
shoeObj.trigger("red",40);
shoeObj.trigger("block",42);
複製代碼

上面的代碼,我們再來運行打印下 如下:

可以看到,訂閱者只訂閱自己感興趣的消息了;

3. 發佈---訂閱模式的代碼封裝

我們知道,對於上面的代碼,小紅去買鞋這麼一個對象shoeObj 進行訂閱,但是如果以後我們需要對買房子或者其他的對象進行訂閱呢,我們需要複製上面的代碼,再重新改下里面的對象代碼;爲此我們需要進行代碼封裝;

如下代碼封裝:

複製代碼
var event = {
    list: [],
    listen: function(key,fn) {
        if(!this.list[key]) {
            this.list[key] = [];
        }
        // 訂閱的消息添加到緩存列表中
        this.list[key].push(fn);
    },
    trigger: function(){
        var key = Array.prototype.shift.call(arguments);
        var fns = this.list[key];
        // 如果沒有訂閱過該消息的話,則返回
        if(!fns || fns.length === 0) {
            return;
        }
        for(var i = 0,fn; fn = fns[i++];) {
            fn.apply(this,arguments);
        }
    }
};
複製代碼

我們再定義一個initEvent函數,這個函數使所有的普通對象都具有發佈訂閱功能,如下代碼:

複製代碼
var initEvent = function(obj) {
    for(var i in event) {
        obj[i] = event[i];
    }
};
// 我們再來測試下,我們還是給shoeObj這個對象添加發布-訂閱功能;
var shoeObj = {};
initEvent(shoeObj);

// 小紅訂閱如下消息
shoeObj.listen('red',function(size){
    console.log("尺碼是:"+size);  
});

// 小花訂閱如下消息
shoeObj.listen('block',function(size){
    console.log("再次打印尺碼是:"+size); 
});
shoeObj.trigger("red",40);
shoeObj.trigger("block",42);
複製代碼

4. 如何取消訂閱事件?

比如上面的列子,小紅她突然不想買鞋子了,那麼對於賣家的店鋪他不想再接受該店鋪的消息,那麼小紅可以取消該店鋪的訂閱。

如下代碼:

複製代碼
event.remove = function(key,fn){
    var fns = this.list[key];
    // 如果key對應的消息沒有訂閱過的話,則返回
    if(!fns) {
        return false;
    }
    // 如果沒有傳入具體的回調函數,表示需要取消key對應消息的所有訂閱
    if(!fn) {
        fn && (fns.length = 0);
    }else {
        for(var i = fns.length - 1; i >= 0; i--) {
            var _fn = fns[i];
            if(_fn === fn) {
                fns.splice(i,1); // 刪除訂閱者的回調函數
            }
        }
    }
};
// 測試代碼如下:
var initEvent = function(obj) {
    for(var i in event) {
        obj[i] = event[i];
    }
};
var shoeObj = {};
initEvent(shoeObj);

// 小紅訂閱如下消息
shoeObj.listen('red',fn1 = function(size){
    console.log("尺碼是:"+size);  
});

// 小花訂閱如下消息
shoeObj.listen('red',fn2 = function(size){
    console.log("再次打印尺碼是:"+size); 
});
shoeObj.remove("red",fn1);
shoeObj.trigger("red",42);
複製代碼

運行結果如下:

5. 全局--發佈訂閱對象代碼封裝

我們再來看看我們傳統的ajax請求吧,比如我們傳統的ajax請求,請求成功後需要做如下事情:

 1. 渲染數據。

 2. 使用數據來做一個動畫。

那麼我們以前肯定是如下寫代碼:

$.ajax(“http://127.0.0.1/index.php”,function(data){
    rendedData(data);  // 渲染數據
    doAnimate(data);  // 實現動畫 
});

假如以後還需要做點事情的話,我們還需要在裏面寫調用的方法;這樣代碼就耦合性很高,那麼我們現在使用發佈-訂閱模式來看如何重構上面的業務需求代碼;

複製代碼
$.ajax(“http://127.0.0.1/index.php”,function(data){
    Obj.trigger(‘success’,data);  // 發佈請求成功後的消息
});
// 下面我們來訂閱此消息,比如我現在訂閱渲染數據這個消息;
Obj.listen(“success”,function(data){
   renderData(data);
});
// 訂閱動畫這個消息
Obj.listen(“success”,function(data){
   doAnimate(data); 
});
複製代碼

爲此我們可以封裝一個全局發佈-訂閱模式對象;如下代碼:

複製代碼
var Event = (function(){
    var list = {},
          listen,
          trigger,
          remove;
          listen = function(key,fn){
            if(!list[key]) {
                list[key] = [];
            }
            list[key].push(fn);
        };
        trigger = function(){
            var key = Array.prototype.shift.call(arguments),
                 fns = list[key];
            if(!fns || fns.length === 0) {
                return false;
            }
            for(var i = 0, fn; fn = fns[i++];) {
                fn.apply(this,arguments);
            }
        };
        remove = function(key,fn){
            var fns = list[key];
            if(!fns) {
                return false;
            }
            if(!fn) {
                fns && (fns.length = 0);
            }else {
                for(var i = fns.length - 1; i >= 0; i--){
                    var _fn = fns[i];
                    if(_fn === fn) {
                        fns.splice(i,1);
                    }
                }
            }
        };
        return {
            listen: listen,
            trigger: trigger,
            remove: remove
        }
})();
// 測試代碼如下:
Event.listen("color",function(size) {
    console.log("尺碼爲:"+size); // 打印出尺碼爲42
});
Event.trigger("color",42);
複製代碼

6. 理解模塊間通信

我們使用上面封裝的全局的發佈-訂閱對象來實現兩個模塊之間的通信問題;比如現在有一個頁面有一個按鈕,每次點擊此按鈕後,div中會顯示此按鈕被點擊的總次數;如下代碼:

<button id="count">點將我</button>

<div id="showcount"></div>

我們中的a.js 負責處理點擊操作 及發佈消息;如下JS代碼:

複製代碼
var a = (function(){
    var count = 0;
    var button = document.getElementById("count");
    button.onclick = function(){
        Event.trigger("add",count++);
    }
})();
複製代碼

b.js 負責監聽add這個消息,並把點擊的總次數顯示到頁面上來;如下代碼:

複製代碼
var b = (function(){
    var div = document.getElementById("showcount");
    Event.listen('add',function(count){
        div.innerHTML = count;
    });
})();
複製代碼

下面是html代碼如下,JS應用如下引用即可:

複製代碼
<!doctype html>
<html lang="en">
 <head>
  <meta charset="UTF-8">
  <title>Document</title>
  <script src="global.js"></script>
 </head>
 <body>
    <button id="count">點將我</button>
    <div id="showcount"></div>
    <script src = "a.js"></script>
    <script src = "b.js"></script>
 </body>
</html>
複製代碼

如上代碼,當點擊一次按鈕後,showcount的div會自動加1,如上演示的是2個模塊之間如何使用發佈-訂閱模式之間的通信問題;

其中global.js 就是我們上面封裝的全局-發佈訂閱模式對象的封裝代碼;

十:理解中介者模式

    先來理解這麼一個問題,假如我們前端開發接的需求是需求方給我們需求,可能一個前端開發會和多個需求方打交道,所以會保持多個需求方的聯繫,那麼在程序裏面就意味着保持多個對象的引用,當程序的規模越大,對象會越來越多,他們之間的關係會越來越複雜,那現在假如現在有一箇中介者(假如就是我們的主管)來對接多個需求方的需求,那麼需求方只需要把所有的需求給我們主管就可以,主管會依次看我們的工作量來給我們分配任務,這樣的話,我們前端開發就不需要和多個業務方聯繫,我們只需要和我們主管(也就是中介)聯繫即可,這樣的好處就弱化了對象之間的耦合。

日常生活中的列子:

    中介者模式對於我們日常生活中經常會碰到,比如我們去房屋中介去租房,房屋中介人在租房者和房東出租者之間形成一條中介;租房者並不關心租誰的房,房東出租者也並不關心它租給誰,因爲有中介,所以需要中介來完成這場交易。

中介者模式的作用是解除對象與對象之間的耦合關係,增加一箇中介對象後,所有的相關對象都通過中介者對象來通信,而不是相互引用,所以當一個對象發送改變時,只需要通知中介者對象即可。中介者使各個對象之間耦合鬆散,而且可以獨立地改變它們之間的交互。

實現中介者的列子如下:

不知道大家有沒有玩過英雄殺這個遊戲,最早的時候,英雄殺有2個人(分別是敵人和自己);我們針對這個遊戲先使用普通的函數來實現如下:

比如先定義一個函數,該函數有三個方法,分別是win(贏), lose(輸),和die(敵人死亡)這三個函數;只要一個玩家死亡該遊戲就結束了,同時需要通知它的對手勝利了; 代碼需要編寫如下:

複製代碼
function Hero(name) {
    this.name = name;
    this.enemy = null; 
}
Hero.prototype.win = function(){
    console.log(this.name + 'Won');
}
Hero.prototype.lose = function(){
    console.log(this.name + 'lose');
}
Hero.prototype.die = function(){
    this.lose();
    this.enemy.win();
}
// 初始化2個對象
var h1 = new Hero("朱元璋");
var h2 = new Hero("劉伯溫");
// 給玩家設置敵人
h1.enemy = h2;
h2.enemy = h1;
// 朱元璋死了 也就輸了
h1.die();  // 輸出 朱元璋lose 劉伯溫Won
複製代碼

現在我們再來爲遊戲添加隊友

比如現在我們來爲遊戲添加隊友,比如英雄殺有6人一組,那麼這種情況下就有隊友,敵人也有3個;因此我們需要區分是敵人還是隊友需要隊的顏色這個字段,如果隊的顏色相同的話,那麼就是同一個隊的,否則的話就是敵人;

我們可以先定義一個數組players來保存所有的玩家,在創建玩家之後,循環players來給每個玩家設置隊友或者敵人;

var players = [];

接着我們再來編寫Hero這個函數;代碼如下:

複製代碼
var players = []; // 定義一個數組 保存所有的玩家
function Hero(name,teamColor) {
    this.friends = [];    //保存隊友列表
    this.enemies = [];    // 保存敵人列表
    this.state = 'live';  // 玩家狀態
    this.name = name;     // 角色名字
    this.teamColor = teamColor; // 隊伍的顏色
}
Hero.prototype.win = function(){
    // 贏了
    console.log("win:" + this.name);
};
Hero.prototype.lose = function(){
    // 輸了
    console.log("lose:" + this.name);
};
Hero.prototype.die = function(){
    // 所有隊友死亡情況 默認都是活着的
    var all_dead = true;
    this.state = 'dead'; // 設置玩家狀態爲死亡
    for(var i = 0,ilen = this.friends.length; i < ilen; i+=1) {
        // 遍歷,如果還有一個隊友沒有死亡的話,則遊戲還未結束
        if(this.friends[i].state !== 'dead') {
            all_dead = false; 
            break;
        }
    }
    if(all_dead) {
        this.lose();  // 隊友全部死亡,遊戲結束
        // 循環 通知所有的玩家 遊戲失敗
        for(var j = 0,jlen = this.friends.length; j < jlen; j+=1) {
            this.friends[j].lose();
        }
        // 通知所有敵人遊戲勝利
        for(var j = 0,jlen = this.enemies.length; j < jlen; j+=1) {
            this.enemies[j].win();
        }
    }
}
// 定義一個工廠類來創建玩家 
var heroFactory = function(name,teamColor) {
    var newPlayer = new Hero(name,teamColor);
    for(var i = 0,ilen = players.length; i < ilen; i+=1) {
        // 如果是同一隊的玩家
        if(players[i].teamColor === newPlayer.teamColor) {
            // 相互添加隊友列表
            players[i].friends.push(newPlayer);
            newPlayer.friends.push(players[i]);
        }else {
            // 相互添加到敵人列表
            players[i].enemies.push(newPlayer);
            newPlayer.enemies.push(players[i]);
        }
    }
    players.push(newPlayer);
    return newPlayer;
};
        // 紅隊
var p1 = heroFactory("aa",'red'),
    p2 = heroFactory("bb",'red'),
    p3 = heroFactory("cc",'red'),
    p4 = heroFactory("dd",'red');
        
// 藍隊
var p5 = heroFactory("ee",'blue'),
    p6 = heroFactory("ff",'blue'),
    p7 = heroFactory("gg",'blue'),
    p8 = heroFactory("hh",'blue');
// 讓紅隊玩家全部死亡
p1.die();
p2.die();
p3.die();
p4.die();
// lose:dd lose:aa lose:bb lose:cc
// win:ee win:ff win:gg win:hh
複製代碼

如上代碼:Hero函數有2個參數,分別是name(玩家名字)和teamColor(隊顏色),

首先我們可以根據隊顏色來判斷是隊友還是敵人;同樣也有三個方法win(贏),lose(輸),和die(死亡);如果每次死亡一個人的時候,循環下該死亡的隊友有沒有全部死亡,如果全部死亡了的話,就輸了,因此需要循環他們的隊友,分別告訴每個隊友中的成員他們輸了,同時需要循環他們的敵人,分別告訴他們的敵人他們贏了;因此每次死了一個人的時候,都需要循環一次判斷他的隊友是否都死亡了;因此每個玩家和其他的玩家都是緊緊耦合在一起了。

下面我們可以使用中介者模式來改善上面的demo;

首先我們仍然定義Hero構造函數和Hero對象原型的方法,在Hero對象的這些原型方法中,不再負責具體的執行的邏輯,而是把操作轉交給中介者對象,中介者對象來負責做具體的事情,我們可以把中介者對象命名爲playerDirector;

在playerDirector開放一個對外暴露的接口ReceiveMessage,負責接收player對象發送的消息,而player對象發送消息的時候,總是把自身的this作爲參數發送給playerDirector,以便playerDirector 識別消息來自於那個玩家對象。

代碼如下:

複製代碼
var players = []; // 定義一個數組 保存所有的玩家
function Hero(name,teamColor) {
    this.state = 'live';  // 玩家狀態
    this.name = name;     // 角色名字
    this.teamColor = teamColor; // 隊伍的顏色
}
Hero.prototype.win = function(){
    // 贏了
    console.log("win:" + this.name);
};
Hero.prototype.lose = function(){
    // 輸了
    console.log("lose:" + this.name);
};
// 死亡
Hero.prototype.die = function(){
    this.state = 'dead';
    // 給中介者發送消息,玩家死亡
    playerDirector.ReceiveMessage('playerDead',this);
}
// 移除玩家
Hero.prototype.remove = function(){
    // 給中介者發送一個消息,移除一個玩家
    playerDirector.ReceiveMessage('removePlayer',this);
};
// 玩家換隊
Hero.prototype.changeTeam = function(color) {
    // 給中介者發送一個消息,玩家換隊
    playerDirector.ReceiveMessage('changeTeam',this,color);
};
// 定義一個工廠類來創建玩家 
var heroFactory = function(name,teamColor) {
    // 創建一個新的玩家對象
    var newHero = new Hero(name,teamColor);
    // 給中介者發送消息,新增玩家
    playerDirector.ReceiveMessage('addPlayer',newHero);
    return newHero;
};
var playerDirector = (function(){
    var players = {},  // 保存所有的玩家
        operations = {}; // 中介者可以執行的操作
    // 新增一個玩家操作
    operations.addPlayer = function(player) {
        // 獲取玩家隊友的顏色
        var teamColor = player.teamColor;
        // 如果該顏色的玩家還沒有隊伍的話,則新成立一個隊伍
        players[teamColor] = players[teamColor] || [];
        // 添加玩家進隊伍
        players[teamColor].push(player);
     };
    // 移除一個玩家
    operations.removePlayer = function(player){
        // 獲取隊伍的顏色
        var teamColor = player.teamColor,
        // 獲取該隊伍的所有成員
        teamPlayers = players[teamColor] || [];
        // 遍歷
        for(var i = teamPlayers.length - 1; i>=0; i--) {
            if(teamPlayers[i] === player) {
                teamPlayers.splice(i,1);
            }
        }
    };
    // 玩家換隊
    operations.changeTeam = function(player,newTeamColor){
        // 首先從原隊伍中刪除
        operations.removePlayer(player);
        // 然後改變隊伍的顏色
        player.teamColor = newTeamColor;
        // 增加到隊伍中
        operations.addPlayer(player);
    };
    // 玩家死亡
operations.playerDead = function(player) {
    var teamColor = player.teamColor,
    // 玩家所在的隊伍
    teamPlayers = players[teamColor];

    var all_dead = true;
    //遍歷 
    for(var i = 0,player; player = teamPlayers[i++]; ) {
        if(player.state !== 'dead') {
            all_dead = false;
            break;
        }
    }
    // 如果all_dead 爲true的話 說明全部死亡
    if(all_dead) {
        for(var i = 0, player; player = teamPlayers[i++]; ) {
            // 本隊所有玩家lose
            player.lose();
        }
        for(var color in players) {
            if(color !== teamColor) {
                // 說明這是另外一組隊伍
                // 獲取該隊伍的玩家
                var teamPlayers = players[color];
                for(var i = 0,player; player = teamPlayers[i++]; ) {
                    player.win(); // 遍歷通知其他玩家win了
                }
            }
        }
    }
};
var ReceiveMessage = function(){
    // arguments的第一個參數爲消息名稱 獲取第一個參數
    var message = Array.prototype.shift.call(arguments);
    operations[message].apply(this,arguments);
};
return {
    ReceiveMessage : ReceiveMessage
};
})();
// 紅隊
var p1 = heroFactory("aa",'red'),
    p2 = heroFactory("bb",'red'),
    p3 = heroFactory("cc",'red'),
        p4 = heroFactory("dd",'red');
        
    // 藍隊
    var p5 = heroFactory("ee",'blue'),
        p6 = heroFactory("ff",'blue'),
        p7 = heroFactory("gg",'blue'),
        p8 = heroFactory("hh",'blue');
    // 讓紅隊玩家全部死亡
    p1.die();
    p2.die();
    p3.die();
    p4.die();
    // lose:aa lose:bb lose:cc lose:dd 
   // win:ee win:ff win:gg win:hh
複製代碼

我們可以看到如上代碼;玩家與玩家之間的耦合代碼已經解除了,而把所有的邏輯操作放在中介者對象裏面進去處理,某個玩家的任何操作不需要去遍歷去通知其他玩家,而只是需要給中介者發送一個消息即可,中介者接受到該消息後進行處理,處理完消息之後會把處理結果反饋給其他的玩家對象。使用中介者模式解除了對象與對象之間的耦合代碼; 使程序更加的靈活.

中介者模式實現購買商品的列子

下面的列子是書上的列子,比如在淘寶或者天貓的列子不是這樣實現的,也沒有關係,我們可以改動下即可,我們最主要來學習下使用中介者模式來實現的思路。

首先先介紹一下業務:在購買流程中,可以選擇手機的顏色以及輸入購買的數量,同時頁面中有2個展示區域,分別顯示用戶剛剛選擇好的顏色和數量。還有一個按鈕動態顯示下一步的操作,我們需要查詢該顏色手機對應的庫存,如果庫存數量小於這次的購買數量,按鈕則被禁用並且顯示庫存不足的文案,反之按鈕高亮且可以點擊並且顯示假如購物車。

HTML代碼如下:

複製代碼
選擇顏色:
    <select id="colorSelect">
        <option value="">請選擇</option>
        <option value="red">紅色</option>
        <option value="blue">藍色</option>
    </select>
    <p>輸入購買的數量: <input type="text" id="numberInput"/></p>
    你選擇了的顏色:<div id="colorInfo"></div>
    <p>你輸入的數量: <div id="numberInfo"></div> </p>
    <button id="nextBtn" disabled="true">請選擇手機顏色和購買數量</button>
複製代碼

首先頁面上有一個select選擇框,然後有輸入的購買數量輸入框,還有2個展示區域,分別是選擇的顏色和輸入的數量的顯示的區域,還有下一步的按鈕操作;

我們先定義一下:

假設我們提前從後臺獲取到所有顏色手機的庫存量

var goods = {
    // 手機庫存
    "red": 6,
    "blue": 8
};

接着 我們下面分別來監聽colorSelect的下拉框的onchange事件和numberInput輸入框的oninput的事件,然後在這兩個事件中作出相應的處理

常規的JS代碼如下:

複製代碼
// 假設我們提前從後臺獲取到所有顏色手機的庫存量
var goods = {
    // 手機庫存
    "red": 6,
    "blue": 8
};
/*
我們下面分別來監聽colorSelect的下拉框的onchange事件和numberInput輸入框的oninput的事件,
然後在這兩個事件中作出相應的處理
*/
var colorSelect = document.getElementById("colorSelect"),
    numberInput = document.getElementById("numberInput"),
    colorInfo = document.getElementById("colorInfo"),
    numberInfo = document.getElementById("numberInfo"),
    nextBtn = document.getElementById("nextBtn");
        
// 監聽change事件
colorSelect.onchange = function(e){
    select();
};
numberInput.oninput = function(){
    select();
};
function select(){
    var color = colorSelect.value,   // 顏色
        number = numberInput.value,  // 數量
        stock = goods[color];  // 該顏色手機對應的當前庫存
            
    colorInfo.innerHTML = color;
    numberInfo.innerHTML = number;

    // 如果用戶沒有選擇顏色的話,禁用按鈕
    if(!color) {
        nextBtn.disabled = true;
        nextBtn.innerHTML = "請選擇手機顏色";
            return;
    }
    // 判斷用戶輸入的購買數量是否是正整數
    var reg = /^\d+$/g;
    if(!reg.test(number)) {
        nextBtn.disabled = true;
        nextBtn.innerHTML = "請輸入正確的購買數量";
        return;
    }
    // 如果當前選擇的數量大於當前的庫存的數量的話,顯示庫存不足
    if(number > stock) {
        nextBtn.disabled = true;
        nextBtn.innerHTML = "庫存不足";
        return;
    }
    nextBtn.disabled = false;
    nextBtn.innerHTML = "放入購物車";
}
複製代碼

上面的代碼雖然是完成了頁面上的需求,但是我們的代碼都耦合在一起了,目前雖然問題不是很多,假如隨着以後需求的改變,SKU屬性越來越多的話,比如頁面增加一個或者多個下拉框的時候,代表選擇手機內存,現在我們需要計算顏色,內存和購買數量,來判斷nextBtn是顯示庫存不足還是放入購物車;代碼如下:

HTML代碼如下:

複製代碼
選擇顏色:
    <select id="colorSelect">
        <option value="">請選擇</option>
        <option value="red">紅色</option>
        <option value="blue">藍色</option>
    </select>
    <br/>
    <br/>
    選擇內存:
    <select id="memorySelect">
        <option value="">請選擇</option>
        <option value="32G">32G</option>
        <option value="64G">64G</option>
    </select>
    <p>輸入購買的數量: <input type="text" id="numberInput"/></p>
    你選擇了的顏色:<div id="colorInfo"></div>
    你選擇了內存:<div id="memoryInfo"></div>
    <p>你輸入的數量: <div id="numberInfo"></div> </p>
    <button id="nextBtn" disabled="true">請選擇手機顏色和購買數量</button>
複製代碼

JS代碼變爲如下:

複製代碼
// 假設我們提前從後臺獲取到所有顏色手機的庫存量
var goods = {
    // 手機庫存
    "red|32G": 6,
    "red|64G": 16,
    "blue|32G": 8,
    "blue|64G": 18
};
/*
我們下面分別來監聽colorSelect的下拉框的onchange事件和numberInput輸入框的oninput的事件,
然後在這兩個事件中作出相應的處理
 */
var colorSelect = document.getElementById("colorSelect"),
    memorySelect = document.getElementById("memorySelect"),
    numberInput = document.getElementById("numberInput"),
    colorInfo = document.getElementById("colorInfo"),
    numberInfo = document.getElementById("numberInfo"),
    memoryInfo = document.getElementById("memoryInfo"),
    nextBtn = document.getElementById("nextBtn");
        
// 監聽change事件
colorSelect.onchange = function(){
    select();
};
numberInput.oninput = function(){
    select();
};
memorySelect.onchange = function(){
    select();    
};
function select(){
    var color = colorSelect.value,   // 顏色
        number = numberInput.value,  // 數量
        memory = memorySelect.value, // 內存
        stock = goods[color + '|' +memory];  // 該顏色手機對應的當前庫存
            
    colorInfo.innerHTML = color;
    numberInfo.innerHTML = number;
    memoryInfo.innerHTML = memory;
    // 如果用戶沒有選擇顏色的話,禁用按鈕
    if(!color) {
        nextBtn.disabled = true;
        nextBtn.innerHTML = "請選擇手機顏色";
            return;
        }
        // 判斷用戶輸入的購買數量是否是正整數
        var reg = /^\d+$/g;
        if(!reg.test(number)) {
            nextBtn.disabled = true;
            nextBtn.innerHTML = "請輸入正確的購買數量";
            return;
        }
        // 如果當前選擇的數量大於當前的庫存的數量的話,顯示庫存不足
        if(number > stock) {
            nextBtn.disabled = true;
            nextBtn.innerHTML = "庫存不足";
            return;
        }
        nextBtn.disabled = false;
        nextBtn.innerHTML = "放入購物車";
    }
複製代碼

一般的代碼就是這樣的,感覺使用中介者模式代碼也類似,這裏就不多介紹了,書上的代碼說有優點,但是個人感覺沒有什麼很大的區別,因此這裏就不再使用中介者模式來編寫代碼了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章