天天看點

前端面試總結九

1.vue的diff算法

Diff 作用

Diff 的出現,就會為了減少更新量,找到最小差異部分DOM,隻更新差異部分DOM就好了,這樣消耗就會小一些,資料變化一下,沒必要把其他沒有涉及的沒有變化的DOM 也替換了。

Diff 做法

Vue 隻會對新舊節點中父節點是相同節點的那一層子節點進行比較,也可以說成是,隻有兩個新舊節點是相同節點的時候,才會去比較他們各自的子節點。

最大的根節點一開始可以直接比較,這也叫做同層級比較,并不需要遞歸,雖然好像降低了一些複用性,也是為了避免過度優化,是一種很高效的 Diff 算法。

新舊節點是什麼

所有的新舊節點指的都是 Vnode 節點,Vue 隻會比較 Vnode 節點,而不是比較 DOM。因為 Vnode 是 JS 對象,不受平台限制,是以以它作為比較基礎,代碼邏輯後期不需要改動,拿到比較結果後,根據不同平台調用相應的方法進行處理就好了。

父節點是相同節點是什麼意思?

比如下圖出現的四次比較(從 first 到 fouth),他們的共同特點都是有相同的父節點

比如藍色方的比較,新舊子節點的父節點是相同節點 1

比如紅色方的比較,新舊子節點的父節點都是 2

是以他們才有比較的機會

前端面試總結九

而下圖中,隻有兩次比較,就是因為在藍色方比較中,并沒有相同節點,是以不會再進行下級子節點比較

前端面試總結九

Diff 比較邏輯

Diff 比較的核心是 節點複用,是以 Diff 比較就是為了在新舊節點中找到相同的節點 ,這個的比較邏輯是建立在上一步說過的同層比較基礎之上的。是以說,節點複用,找到相同節點并不是無限制遞歸查找。

比如下圖中,的确舊節點樹和新節點樹中有相同節點 6,但是然并卵,舊節點6并不會被複用。

前端面試總結九

就算在同一層級,然而父節點不一樣,依舊然并卵

前端面試總結九

隻有這種情況的節點會被複用,相同父節點 8

前端面試總結九

Diff 的比較邏輯

(1)能不移動,盡量不移動

(2)沒得辦法,隻好移動

(3)實在不行,建立或删除

比較處理流程是下面這樣

在新舊節點中

(1)先找到不需要移動的相同節點,消耗最小

(2)再找相同但是需要移動的節點,消耗第二小

(3)最後找不到,才會去建立删除節點,保底處理

比較是為了修改DOM 樹

其實這裡存在三種樹,一個是頁面DOM 樹,一個是舊VNode 樹,一個是新Vnode 樹。頁面DOM 樹和舊VNode 樹節點一一對應的,而新Vnode 樹則是表示更新後頁面DOM 樹該有的樣子。這裡把舊Vnode 樹和新Vnode樹進行比較的過程中,不會對這兩棵Vode樹進行修改,而是以比較的結果直接對真實DOM 進行修改。

比如說,在舊 Vnode 樹同一層中,找到和新Vnode 樹中一樣但位置不一樣節點,此時需要移動這個節點,但是不是移動舊 Vnode 樹中的節點,而是直接移動DOM。

總的來說,新舊 Vnode 樹是拿來比較的,頁面DOM樹是拿來根據比較結果修改的。

Diff 簡單例子

比如下圖存在這兩棵需要比較的新舊節點樹和一棵需要修改的頁面 DOM樹

前端面試總結九

第一輪比較開始

因為父節點都是 1,是以開始比較他們的子節點,按照我們上面的比較邏輯,是以先找相同 && 不需移動 的點,毫無疑問,找到 2。

前端面試總結九

拿到比較結果,這裡不用修改DOM,是以 DOM 保留在原地

前端面試總結九

第二輪比較開始

然後,沒有相同 &&不需移動 的節點 了,隻能第二個方案,開始找相同的點,找到節點5,相同但是位置不同,是以需要移動。

前端面試總結九

拿到比較結果,頁面DOM樹需要移動DOM了,不修改,原樣移動

前端面試總結九

第三輪比較開始

相同節點也沒得了,沒得辦法了,隻能建立了,是以要根據新Vnode 中沒找到的節點去建立并且插入,然後舊Vnode 中有些節點不存在新VNode 中,是以要删除。

前端面試總結九

于是開始建立節點 6 和 9,并且删除節點 4 和 5

前端面試總結九

然後頁面就完成更新啦。

建立執行個體 到 開始Diff 的流程

首先,當你建立執行個體的時候,比如這樣

前端面試總結九

你調用一個 Vue 函數,是以來看下 Vue 函數

function Vue() {    
    ... 已省略其他
    new Watcher(function() {
        vm._update(vm._render());
    })
    ... 已省略其他
}
           

函數中做了兩件事

(1)為執行個體建立一個 watcher

(2)為 watcher 綁定更新回調(就是 new Watcher 傳入的 function )

每個執行個體都會有一個專屬的 watcher,而綁定的回調,在頁面更新時會調用。

我們現在來看下簡化的 Watcher 的源碼

funciton Watcher(expOrFn){    
    this.getter = expOrFn;    
    this.get();
}

Watcher.prototype.get = function () {    
    this.getter()
}
           

watcher 會儲存更新回調,并且在建立 watcher 的時候就會立刻調用一遍更新回調

現在我們繼續看 更新回調的内容

vm._render

生成頁面模闆對應的 Vnode 樹,比如

前端面試總結九

生成的 Vnode 樹是( 其中num的值是111 )

{    
    tag: "div",    
    children:[{        
        tag: "span"
    },{        
        tag: undefined,        
        text: "111"
    }]
}
           

vm._update

比較 舊Vnode 樹和 vm._render 生成的新 Vnode 樹進行比較

比較完後,更新頁面的DOM,進而完成更新

Vue.prototype._update = function(vnode) {  
    var vm = this;    
    var prevEl = vm.$el;    
    var prevVnode = vm._vnode;
    vm._vnode = vnode;    

    // 不存在舊節點
    if (!prevVnode) {
        vm.$el = vm.__patch__(
            vm.$el, vnode,
            vm.$options._parentElm,
            vm.$options._refElm
        );
    }    
    else {
        vm.$el = vm.__patch__(
            prevVnode, vnode
        );
    }
};
           

解釋其中幾個點

vm._vnode

這個屬性儲存的就是目前 Vnode 樹,當頁面開始更新,而生成了新的 Vnode 樹之後,這個屬性則會替換成新的Vnode.是以儲存在這裡,是為了友善拿到舊 Vnode 樹.

vm.patch

var patch = createPatchFunction();
Vue.prototype.__patch__ =  patch ;
           

是經過一個 createPatchFunciton 生成的,然後指派到 Vue 的原型上,是以可以

vm.__patch__

調用。

createPatchFunction

function createPatchFunction() {  
    return function patch(
        oldVnode, vnode, parentElm, refElm    
    ) {      
        // 沒有舊節點,直接生成新節點
        if (!oldVnode) {
            createElm(vnode, parentElm, refElm);
        } 
        else {     
            // 且是一樣 Vnode
            if (sameVnode(oldVnode, vnode)) {                
                // 比較存在的根節點
                patchVnode(oldVnode, vnode);
            } 
            else {    
                // 替換存在的元素
                var oldElm = oldVnode.elm;                
                var _parentElm = oldElm.parentNode    
                // 建立新節點
                createElm(vnode, _parentElm, oldElm.nextSibling);   
                // 銷毀舊節點
                if (_parentElm) {
                    removeVnodes([oldVnode], 0, 0);
                }
            }
        }        
        return vnode.elm
    }
}
           

這個函數的作用就是,比較新節點和舊節點有什麼不同,然後完成更新,是以你看到接收一個 oldVnode 和 vnode,處理的流程分為

(1)沒有舊節點

沒有舊節點,說明是頁面剛開始初始化的時候,此時,根本不需要比較了,直接全部都是建立,是以隻調用 createElm。

(2)舊節點和新節點自身一樣(不包括其子節點)

通過 sameVnode 判斷節點是否一樣,舊節點和新節點自身一樣時,直接調用 patchVnode 去處理這兩個節點。

當兩個Vnode自身一樣的時候,我們需要做什麼?

首先,自身一樣,我們可以先簡單了解,是 Vnode 的兩個屬性 tag 和 key 一樣。那麼,我們是不知道其子節點是否一樣的,是以肯定需要比較子節點。是以,patchVnode 其中的一個作用,就是比較子節點

(3)舊節點和新節點自身不一樣

當兩個節點不一樣的時候,不難了解,直接建立新節點,删除舊節點。

patchVnode

function patchVnode(oldVnode, vnode) { 
    if (oldVnode === vnode) return
    var elm = vnode.elm = oldVnode.elm;    
    var oldCh = oldVnode.children;    
    var ch = vnode.children;   
    // 更新children
    if (!vnode.text) {   
        // 存在 oldCh 和 ch 時
        if (oldCh && ch) {            
            if (oldCh !== ch) 
                updateChildren(elm, oldCh, ch);
        }    
        // 存在 newCh 時,oldCh 隻能是不存在,如果存在,就跳到上面的條件了
        else if (ch) {   
            if (oldVnode.text) elm.textContent = '';      
            for (var i = 0; i <= ch.length - 1; ++i) {
                createElm(
                  ch[i],elm, null
                );
            }
        } 
        else if (oldCh) {     
            for (var i = 0; i<= oldCh.length - 1; ++i) {            
                oldCh[i].parentNode.removeChild(el);
            }
        } 
        else if (oldVnode.text) {
            elm.textContent = '';
        }
    } 
    else if (oldVnode.text !== vnode.text) {
        elm.textContent = vnode.text;
    }
}
           

正如我們所想,這個函數的确會去比較處理子節點。總的來說,這個函數的作用是

(1)Vnode 是文本節點,則更新文本(文本節點不存在子節點)

當 VNode 存在 text 這個屬性的時候,就證明了 Vnode 是文本節點。我們可以先來看看 文本類型的 Vnode 是什麼樣子

前端面試總結九

是以當 Vnode 是文本節點的時候,需要做的就是,更新文本,同樣有兩種處理

1)當新Vnode.text 存在,而且和舊 VNode.text 不一樣時

直接更新這個 DOM 的文本内容

注:textContent 是 真實DOM 的一個屬性, 儲存的是 dom 的文本,是以直接更新這個屬性

2)新Vnode 的 text 為空,直接把文本DOM 指派給空

(2)Vnode 有子節點,則處理比較更新子節點

當 Vnode 存在子節點的時候,因為不知道新舊節點的子節點是否一樣,是以需要比較,才能完成更新,這裡有三種處理

1)新舊節點都有子節點,而且不一樣

又出現了一個新函數,那就是 updateChildren,這個函數非常的重要,是 Diff 的核心子產品,蘊含着 Diff 的思想。

我們先來思考下 updateChildren 的作用。記得條件,當新節點和舊節點都存在,要怎麼去比較才能知道有什麼不一樣呢?使用周遊,新子節點和舊子節點一個個比較。如果一樣,就不更新,如果不一樣,就更新。

2)隻有新節點

隻有新節點,不存在舊節點,那麼沒得比較了,所有節點都是全新的,是以直接全部建立就好了,建立是指建立出所有新DOM,并且添加進父節點的。

3)隻有舊節點

隻有舊節點而沒有新節點,說明更新後的頁面,舊節點全部都不見了。那麼要做的,就是把所有的舊節點删除,也就是直接把DOM 删除。

updateChildren

function updateChildren(parentElm, oldCh, newCh) {
    var oldStartIdx = 0;    
    var oldEndIdx = oldCh.length - 1;    
    var oldStartVnode = oldCh[0];    
    var oldEndVnode = oldCh[oldEndIdx];    
    var newStartIdx = 0;    
    var newEndIdx = newCh.length - 1;    
    var newStartVnode = newCh[0];    
    var newEndVnode = newCh[newEndIdx];    
    var oldKeyToIdx, idxInOld, vnodeToMove, refElm;

    // 不斷地更新 OldIndex 和 OldVnode ,newIndex 和 newVnode
    while (
        oldStartIdx <= oldEndIdx && 
        newStartIdx <= newEndIdx
    ) {        
        if (!oldStartVnode) {
            oldStartVnode = oldCh[++oldStartIdx];
        }     
        else if (!oldEndVnode) {
            oldEndVnode = oldCh[--oldEndIdx];
        }   
        //  舊頭 和新頭 比較
        else if (sameVnode(oldStartVnode, newStartVnode)) {
            patchVnode(oldStartVnode, newStartVnode);
            oldStartVnode = oldCh[++oldStartIdx];
            newStartVnode = newCh[++newStartIdx];
        }    
        //  舊尾 和新尾 比較
        else if (sameVnode(oldEndVnode, newEndVnode)) {
            patchVnode(oldEndVnode, newEndVnode);
            oldEndVnode = oldCh[--oldEndIdx];
            newEndVnode = newCh[--newEndIdx];
        }                               
        // 舊頭 和 新尾 比較
        else if (sameVnode(oldStartVnode, newEndVnode)) {
            patchVnode(oldStartVnode, newEndVnode);            
            // oldStartVnode 放到 oldEndVnode 後面,還要找到 oldEndValue 後面的節點
            parentElm.insertBefore(
                oldStartVnode.elm, 
                oldEndVnode.elm.nextSibling
            );            
            oldStartVnode = oldCh[++oldStartIdx];
            newEndVnode = newCh[--newEndIdx];
        }   
        //  舊尾 和新頭 比較
        else if (sameVnode(oldEndVnode, newStartVnode)) {
            patchVnode(oldEndVnode, newStartVnode);            
            // oldEndVnode 放到 oldStartVnode 前面
            parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm);
            oldEndVnode = oldCh[--oldEndIdx];
            newStartVnode = newCh[++newStartIdx];
        }        
        // 單個新子節點 在 舊子節點數組中 查找位置
        else {    
            // oldKeyToIdx 是一個 把 Vnode 的 key 和 index 轉換的 map
            if (!oldKeyToIdx) {
                oldKeyToIdx = createKeyToOldIdx(
                    oldCh, oldStartIdx, oldEndIdx
                );
            }     
            // 使用 newStartVnode 去 OldMap 中尋找 相同節點,預設key存在
            idxInOld = oldKeyToIdx[newStartVnode.key]        
            //  新孩子中,存在一個新節點,老節點中沒有,需要建立 
            if (!idxInOld) {  
                //  把  newStartVnode 插入 oldStartVnode 的前面
                createElm(
                    newStartVnode, 
                    parentElm, 
                    oldStartVnode.elm
                );
            }            
            else {                
                //  找到 oldCh 中 和 newStartVnode 一樣的節點
                vnodeToMove = oldCh[idxInOld];     
                if (sameVnode(vnodeToMove, newStartVnode)) {
                    patchVnode(vnodeToMove, newStartVnode);                
                    // 删除這個 index
                    oldCh[idxInOld] = undefined;                    
                    // 把 vnodeToMove 移動到  oldStartVnode 前面
                    parentElm.insertBefore(
                        vnodeToMove.elm, 
                        oldStartVnode.elm
                    );
                }                
                // 隻能建立一個新節點插入到 parentElm 的子節點中
                else {                    
                    // same key but different element. treat as new element
                    createElm(
                        newStartVnode, 
                        parentElm, 
                        oldStartVnode.elm
                    );
                }
            }            
            // 這個新子節點更新完畢,更新 newStartIdx,開始比較下一個
            newStartVnode = newCh[++newStartIdx];
        }
    }    
    // 處理剩下的節點
    if (oldStartIdx > oldEndIdx) {  
        var newEnd = newCh[newEndIdx + 1]
        refElm = newEnd ? newEnd.elm :null;        
        for (; newStartIdx <= newEndIdx; ++newStartIdx) {
            createElm(
               newCh[newStartIdx], parentElm, refElm
            );
        }
    }    

    // 說明新節點比對完了,老節點可能還有,需要删除剩餘的老節點
    else if (newStartIdx > newEndIdx) {       
        for (; oldStartIdx<=oldEndIdx; ++oldStartIdx) {
            oldCh[oldStartIdx].parentNode.removeChild(el);
        }
    }
}
           

首先要明确這個函數處理的是什麼

處理的是新子節點和舊子節點,循環周遊逐個比較

如何循環周遊?

(1)使用 while

(2)新舊節點數組都配置首尾兩個索引

新節點的兩個索引:newStartIdx , newEndIdx

舊節點的兩個索引:oldStartIdx,oldEndIdx

以兩邊向中間包圍的形式來進行周遊

頭部的子節點比較完畢,startIdx 就加1

尾部的子節點比較完畢,endIdex 就減1

隻要其中一個數組周遊完(startIdx<endIdx),則結束周遊

前端面試總結九

源碼處理的流程分為兩個

(1)比較新舊子節點

注:這裡有兩個數組,一個是新子Vnode數組,一個舊子Vnode數組。在比較過程中,不會對兩個數組進行改變(比如不會插入,不會删除其子項),而所有比較過程中都是直接插入删除 真實頁面DOM。

我們明确一點,比較的目的是什麼?

找到 新舊子節點中的相同的子節點,盡量以移動替代建立去更新DOM,隻有在實在不同的情況下,才會建立。

比較更新計劃步驟

首先考慮,不移動DOM;其次考慮,移動DOM;最後考慮,建立 / 删除 DOM。能不移動,盡量不移動。不行就移動,實在不行就建立。

下面開始說源碼中的比較邏輯:

五種比較邏輯如下

1)舊頭 == 新頭

當兩個新舊的兩個頭一樣的時候,并不用做什麼處理,符合我們的步驟第一條,不移動DOM完成更新。但是看到一句,patchVnode,就是為了繼續處理這兩個相同節點的子節點,或者更新文本。因為我們不考慮多層DOM 結構,是以 新舊兩個頭一樣的話,這裡就算結束了,可以直接進行下一輪循環。

前端面試總結九

2)舊尾 == 新尾

和頭頭相同的處理是一樣的,尾尾相同,直接跳入下個循環。

前端面試總結九

3)舊頭 == 新尾

這步不符合不移動DOM,是以隻能移動DOM 了。

源碼是這樣的

parentElm.insertBefore(
    oldStartVnode.elm, 
    oldEndVnode.elm.nextSibling
);
           

以新子節點的位置來移動的,舊頭在新子節點的末尾,是以把 oldStartVnode 的 dom 放到 oldEndVnode 的後面。但是因為沒有把dom 放到誰後面的方法,是以隻能使用 insertBefore

,即放在 oldEndVnode 後一個節點的前面。

圖示是這樣的

前端面試總結九

然後更新兩個索引

4)舊尾 == 新頭

同樣不符合不移動DOM,也隻能移動DOM 了

parentElm.insertBefore(
    oldEndVnode.elm, 
    oldStartVnode.elm
);
           

把 oldEndVnode DOM 直接放到目前 oldStartVnode.elm 的前面。

圖示是這樣的

前端面試總結九

然後更新兩個索引

5)單個查找

目前面四種比較邏輯都不行的時候,這是最後一種處理方法。拿新子節點的子項,直接去舊子節點數組中周遊,找一樣的節點出來。

流程大概是

1、生成舊子節點數組以 vnode.key 為key 的 map 表

這個map 表的作用,就主要是判斷存在什麼舊子節點,比如你的舊子節點數組是

[{    
    tag:"div",  key:1
},{  
    tag:"strong", key:2
},{  
    tag:"span",  key:4
}]
           

經過 createKeyToOldIdx 生成一個 map 表 oldKeyToIdx,{ vnodeKey: 數組Index },屬性名是 vnode.key,屬性值是該 vnode 在children 的位置是這樣

oldKeyToIdx = {
    1:0,
    2:1,
    4:2
}
           

2、拿到新子節點數組中一個子項,判斷它的key是否在上面的map 中

拿到新子節點中的 子項Vnode,然後拿到它的 key,去比對map 表,判斷是否有相同節點。

3、不存在,則建立DOM

直接建立DOM,并插入oldStartVnode 前面。

前端面試總結九

4、存在,繼續判斷是否 sameVnode

找到這個舊子節點,然後判斷和新子節點是否 sameVnode,如果相同,直接移動到 oldStartVnode 前面,如果不同,直接建立插入 oldStartVnode 前面。

我們上面說了比較子節點的處理的流程分為兩個

①比較新舊子節點

②比較完畢,處理剩下的節點

(2)比較完畢,處理剩下的節點

在updateChildren 中,比較完新舊兩個數組之後,可能某個數組會剩下部分節點沒有被處理過,是以這裡需要統一處理

1 新子節點周遊完了

newStartIdx > newEndIdx
           

新子節點周遊完畢,舊子節點可能還有剩,是以我們要對可能剩下的舊節點進行批量删除!

就是周遊剩下的節點,逐個删除DOM。

for (; oldStartIdx <= oldEndIdx; ++oldStartIdx) {
    oldCh[oldStartIdx]
    .parentNode
    .removeChild(el);
}
           
前端面試總結九

2舊子節點周遊完了

oldStartIdx > oldEndIdx
           

舊子節點周遊完畢,新子節點可能有剩,是以要對剩餘的新子節點處理。很明顯,剩餘的新子節點不存在舊子節點中,是以全部建立。

for (; newStartIdx <= newEndIdx; ++newStartIdx) {
   createElm(
      newCh[newStartIdx], 
      parentElm, 
      refElm
   );
}
           

但是建立有一個問題,就是插在哪裡?

是以其中的 refElm 就成了疑點,看下源碼

var newEnd = newCh[newEndIdx + 1]
refElm = newEnd ? newEnd.elm :null;
           

refElm 擷取的是 newEndIdx 後一位的節點,目前沒有處理的節點是 newEndIdx。也就是說 newEndIdx+1 的節點如果存在的話,肯定被處理過了。如果 newEndIdx 沒有移動過,一直是最後一位,那麼就不存在 newChnewEndIdx + 1。那麼 refElm 就是空,那麼剩餘的新節點就全部添加進 父節點孩子的末尾,相當于

for (; newStartIdx <= newEndIdx; ++newStartIdx) {     
    parentElm.appendChild(
        newCh[newStartIdx]
    );
}
           

如果 newEndIdx 移動過,那麼就逐個添加在 refElm 的前面,相當于

for (; newStartIdx <= newEndIdx; ++newStartIdx) {
    parentElm.insertBefore(
        newCh[newStartIdx] ,
        refElm 
    );
}
           

如圖

前端面試總結九

走流程

以下的節點,綠色表示未處理,灰色表示已經處理,淡綠色表示正在處理,紅色表示新插入,如下

前端面試總結九

現在Vue 需要更新,存在下面兩組新舊子節點,需要進行比較,來判斷需要更新哪些節點

前端面試總結九

1頭頭比較,節點一樣,不需移動,隻用更新索引

前端面試總結九

更新索引,newStartIdx++ , oldStartIdx++

開始下輪處理

一系列判斷之後,【舊頭 2】 和 【 新尾 2】相同,直接移動到 oldEndVnode 後面

前端面試總結九

更新索引,newEndIdx-- ,oldStartIdx ++

開始下輪處理

3一系列判斷之後,【舊頭 2】 和 【 新尾 2】相同,直接移動到 oldStartVnode 前面

前端面試總結九

更新索引,oldEndIdx-- ,newStartIdx++

開始下輪比較

4隻剩一個節點,走到最後一個判斷,單個查找

找不到一樣的,直接建立插入到 oldStartVnode 前面

前端面試總結九

更新索引,newStartIdx++

此時 newStartIdx> newEndIdx ,結束循環

5 批量删除可能剩下的老節點

此時看 舊 Vnode 數組中, oldStartIdx 和 oldEndIdx 都指向同一個節點,是以隻用删除 oldVnode-4 這個節點。

ok,完成所有比較流程。

參考:【Vue原理】Diff - 源碼版 之 從建立執行個體到開始diff

【Vue原理】Diff - 源碼版 之 Diff 流程

2.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點:第一:弱化對象間的耦合,防止代碼的重複。在一個方法中進行類的執行個體化,可以消除重複性的代碼。第二:重複性的代碼可以放在父類去編寫,子類繼承于父類的所有成員屬性和方法,子類隻專注于實作自己的業務邏輯。

了解單體模式

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

單體模式的優點是:

  • 可以用來劃分命名空間,減少全局變量的數量。
  • 使用單體模式可以使代碼組織的更為一緻,使代碼容易閱讀和維護。
  • 可以被執行個體化,且執行個體化一次。

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

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

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

// 對象字面量
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

了解代理模式

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

代理的優點:

代理對象可以代替本體被執行個體化,并使其可以被遠端通路;

它還可以把本體執行個體化推遲到真正需要的時候;對于執行個體化比較費時的本體對象,或者因為尺寸比較大以至于不用時不适于儲存在記憶體中的本體,我們可以推遲執行個體化該對象;

我們先來了解代理對象代替本體對象被執行個體化的列子;比如現在京東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方法設定圖檔的路徑;他們之間的優缺點如下:

第一種方案一般的方法代碼的耦合性太高,一個函數内負責做了幾件事情,比如建立img元素,和實作給未加載圖檔完成之前設定loading加載狀态等多項事情,未滿足面向對象設計原則中單一職責原則;并且當某個時候不需要代理的時候,需要從myImage 函數内把代碼删掉,這樣代碼耦合性太高。

第二種方案使用代理模式,其中myImage 函數隻負責做一件事,建立img元素加入到頁面中,其中的加載loading圖檔交給代理函數ProxyImage 去做,當圖檔加載成功後,代理函數ProxyImage 會通知及執行myImage 函數的方法,同時當以後不需要代理對象的話,我們直接可以調用本體對象的方法即可;

從上面代理模式我們可以看到,代理模式和本體對象中有相同的方法setSrc,這樣設定的話有如下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小時内發貨等,使用者更不關心快遞是給誰派送,當然有的人會關心是什麼快遞送貨的; 對于使用者來說,隻要在規定的時間内發貨,且一般能在相當的時間内收到貨就可以,當然指令模式也有撤銷指令和重做指令,比如我們下單後,我突然不想買了,我在發貨之前可以取消訂單,也可以重新下單(也就是重做指令);比如我的衣服尺碼拍錯了,我取消該訂單,重新拍一個大碼的。

指令模式的列子

記得我以前剛做前端的那會兒,也就是剛畢業進的第一家公司,進的是做外包項目的公司,該公司一般外包淘寶活動頁面及騰訊的遊戲頁面,我們那會兒應該叫切頁面的前端,負責做一些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);
           

了解宏指令:

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

其實類似把頁面的所有函數方法放在一個數組裡面去,然後周遊這個數組,依次執行該方法的。

代碼如下:

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中模闆方法模式;是以我們現在回到正題上來;

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

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

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

(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.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

那麼代碼改成如下:

(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中的政策模式

了解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語句;

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

使用組合函數重構代碼

組合函數是把各種算法封裝到一個個的小函數裡面,比如等級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等級的時候,而且缺乏彈性。

使用政策模式重構代碼

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

一個基于政策模式的程式至少由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
           

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

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)支援簡單的廣播通信,當對象狀态發生改變時,會自動通知已經訂閱過的對象。

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

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

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

釋出訂閱模式的缺點:

建立訂閱者需要消耗一定的時間和記憶體。

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

如何實作釋出–訂閱模式?

(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);
           

釋出—訂閱模式的代碼封裝

我們知道,對于上面的代碼,小紅去買鞋這麼一個對象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);
           

如何取消訂閱事件?

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

如下代碼:

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);
           

全局–釋出訂閱對象代碼封裝

我們再來看看我們傳統的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);
           

了解子產品間通信

我們使用上面封裝的全局的釋出-訂閱對象來實作兩個子產品之間的通信問題;比如現在有一個頁面有一個按鈕,每次點選此按鈕後,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 = "放入購物車";
    }
           

參考:詳解 Javascript十大常用設計模式

繼續閱讀