Javascript設計模式詳解

龍恩0707發表於2016-02-18

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 = "放入購物車";
    }

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

相關文章