JS常用設計模式解析01-單例模式

1.實例演進

考慮實現如下功能,點擊一個按鈕后出現一個遮罩層。
原始辦法:我們只需要實現一個創(chuàng)建遮罩層的函數并將其作為按鈕點擊的回調事件即可。如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>createMask</title>
    <script>
        function createMask() {
            var mask = document.createElement('div');
            mask.style.cssText = 'display:none;position:absolute;top:0;bottom:0;left:0;right:0;background:#000;opacity:0.2;z-index:999;'
            mask.addEventListener('click', function () {
                this.style.display = 'none';
            });
            document.body.appendChild(mask);
            return mask;
        }
        window.onload = function() {
            document.getElementById('button').addEventListener('click', function() {
                var mask = createMask();
                mask.style.display = 'block';
            });
        }
    </script>
</head>
<body>
<button id="button">click to create a mask</button>
</body>
</html>

這里我們來看看效果:


原始方法

可以看到,每次點擊都會創(chuàng)建一個新的遮罩層。而且老的遮罩層也仍然存在。這會無限增大html的體積。

改進辦法1:將每次點擊遮罩層隱藏改為將其移除。即:

mask.addEventListener('click', function () {
    document.body.removeChild(this);
});

具體效果這里就不演示了。
但即使這樣,我們每一次點擊仍然會創(chuàng)建一個新的遮罩層,損耗性能。

改進辦法2:在頁面初始化時建立一個隱藏的遮罩,每次點擊只是控制其display屬性。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>createMask</title>
    <script>
        function createMask() {
            var mask = document.createElement('div');
            mask.style.cssText = 'display:none;position:absolute;top:0;bottom:0;left:0;right:0;background:#000;opacity:0.2;z-index:999;'
            mask.addEventListener('click', function () {
                this.style.display = 'none';
            });
            document.body.appendChild(mask);
            return mask;
        }
        window.onload = function() {
            var mask = createMask();
            document.getElementById('button').addEventListener('click', function() {
                mask.style.display = 'block';
            });
        }
    </script>
</head>
<body>
<button id="button">click to create a mask</button>
</body>
</html>

這樣的話就不用每次點擊按鈕都新創(chuàng)建一個遮罩層了,可是還有一個缺點,那就是,如果用戶并沒有點擊按鈕,這個遮罩層不是白白創(chuàng)建了嗎。

改進辦法3:點擊按鈕的時候,動態(tài)判斷是否需要新建一個遮罩層

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>createMask</title>
    <script>
        var mask;
        function createMask() {
            if (mask) {
                return mask;
            }
            mask = document.createElement('div');
            mask.style.cssText = 'display:none;position:absolute;top:0;bottom:0;left:0;right:0;background:#000;opacity:0.2;z-index:999;'
            mask.addEventListener('click', function () {
                this.style.display = 'none';
            });
            document.body.appendChild(mask);
            return mask;
        }
        window.onload = function() {
            document.getElementById('button').addEventListener('click', function() {
                mask = createMask();
                mask.style.display = 'block';
            });
        }
    </script>
</head>
<body>
<button id="button">click to create a mask</button>
</body>
</html>

這樣看上去已經很不錯了,可是問題還是有,那就是mask成為了一個全局變量。
改進辦法4:將mask當做局部變量,createMask當做閉包來引用。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>createMask</title>
    <script>
        var createMask = (function () {
            var mask;
            return function () {
                if (mask) {
                    return mask;
                }
                mask = document.createElement('div');
                mask.style.cssText = 'display:none;position:absolute;top:0;bottom:0;left:0;right:0;background:#000;opacity:0.2;z-index:999;'
                mask.addEventListener('click', function () {
                    this.style.display = 'none';
                });
                document.body.appendChild(mask);
                return mask;
            }
        })();

        window.onload = function() {
            document.getElementById('button').addEventListener('click', function() {
                var mask = createMask();
                mask.style.display = 'block';
            });
        }
    </script>
</head>
<body>
<button id="button">click to create a mask</button>
</body>
</html>

到這里,我們的代碼已經很不錯了。然而,設想這樣一個場景,你在不同的頁面,需要使用不同背景顏色的mask。怎么辦?一個簡單的想法,就是像createMask里面?zhèn)鲄???墒?,你又有了新的需求,不同頁面還需要不同的透明度,也簡單,再增加一個參數。那么問題來了,第一,你不可能無限制地為函數增加參數,第二,你的兩個頁面需要創(chuàng)建的mask可能是根本不一樣的,比如另一個mask是一張圖片,和前一種mask的創(chuàng)建方法沒有什么共同性。那么這里最好的辦法其實就是定義不同的創(chuàng)建mask的方法,然后根據需要使用和不同的創(chuàng)建方法。
改進辦法5:抽象成更通用的單例模式

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>createMask</title>
    <script>
        var maskMethod1 = function() {
            var mask;
            mask = document.createElement('div');
            mask.style.cssText = 'display:none;position:absolute;top:0;bottom:0;left:0;right:0;background:#000;opacity:0.2;z-index:999;'
            mask.addEventListener('click', function () {
                this.style.display = 'none';
            });
            document.body.appendChild(mask);
            return mask;
        };
        var maskMethod2 = function() {
            var mask;
            mask = document.createElement('div');
            mask.style.cssText = 'display:none;position:absolute;top:0;bottom:0;left:0;right:0;background:#abc;opacity:0.6;z-index:999;'
            mask.addEventListener('click', function () {
                this.style.display = 'none';
            });
            document.body.appendChild(mask);
            return mask;
        };
        var mask;
        var createMask = function (fn) {
            return mask || (mask = fn.apply(this,arguments));
        };

        window.onload = function() {
            document.getElementById('button').addEventListener('click', function() {
                var mask = createMask(maskMethod2);
                mask.style.display = 'block';
            });
        }
    </script>
</head>
<body>
<button id="button">click to create a mask</button>
</body>
</html>

但是這里,為了使用 createMask的時候可以動態(tài)傳參,我引入了一個全局變量。不知道有沒有同學知道這里該如何不引入全局變量且能支持傳參呢?如果知道的同學,還請不吝賜教哈
(找到辦法了,寫這篇文章的時候我還沒有看到《JavaScript設計模式與開發(fā)實踐》這本書,看過以后,發(fā)現這一章和作者的思路還是挺接近的,但是作者的分析更加全面和精辟。而且,作者也沒有通過引入全局變量來進行抽象,建議大家看一下這本書。真的很精辟。強烈推薦。)
改進辦法6:利用閉包抽象成更通用的單例模式

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>createMask</title>
    <script>
        var maskMethod1 = function() {
            var mask;
            mask = document.createElement('div');
            mask.style.cssText = 'display:none;position:absolute;top:0;bottom:0;left:0;right:0;background:#000;opacity:0.2;z-index:999;'
            mask.addEventListener('click', function () {
                this.style.display = 'none';
            });
            document.body.appendChild(mask);
            return mask;
        };
        var maskMethod2 = function() {
            var mask;
            mask = document.createElement('div');
            mask.style.cssText = 'display:none;position:absolute;top:0;bottom:0;left:0;right:0;background:#abc;opacity:0.6;z-index:999;'
            mask.addEventListener('click', function () {
                this.style.display = 'none';
            });
            document.body.appendChild(mask);
            return mask;
        };

        var getMaskCreate = function (fn) {
            var mask;
            return function() {
                return mask || (mask = fn.apply(this,arguments));
            }
        };

        window.onload = function() {
            var createMask = getMaskCreate(maskMethod2);
            document.getElementById('button').addEventListener('click', function() {
                var mask = createMask();
                mask.style.display = 'block';
            });
        }
    </script>
</head>
<body>
<button id="button">click to create a mask</button>
</body>
</html>

2. 單例模式的思想與優(yōu)點

由第1節(jié)的遮罩層例子,引出單例模式的設計思想,其實質就是:保證一個類僅有一個實例,并且提供一個訪問它的全局訪問點。
單體模式具有如下優(yōu)點:

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

3. 單例模式的實現

單例模式的基本結構:

var Singleton = function(name){
    this.name = name;
    this.instance = null;
};
Singleton.prototype.getName = function(){
    return this.name;
}
/* *
 * 1.這里的this在非嚴格模式下指向全局變量
 * 2. 用this而不用window可以根據宿主指向全局變量,比如node是global
 * 3. 使用這種寫法不能使用new直接調用
*/
function getInstance(name) {
    if(!this.instance) {
        this.instance = new Singleton(name);
    }
    return this.instance;
}
// 這里不能直接通過new來調用
var a = getInstance("a");
var b = getInstance("b");
// 證明該對象僅可被實例化一次
console.log(a === b);  // true
// 證明創(chuàng)建了一個額外的全局變量
console.log(window.instance); // Singleton {name: "a", instance: null}
console.log(a === window.instance);  // true

這種模式很好理解,但是額外創(chuàng)建了一個全局變量。

閉包實現單例模式

var Singleton = function(name){
    this.name = name;
};
Singleton.prototype.getName = function(){
    return this.name;
}
// 使用閉包,使instance不再暴露到全局
var getInstance = (function() {
    var instance = null;
    return function(name) {
        if(!instance) {
            instance = new Singleton(name);
        }
        return instance;
    }
})();
// 這里可以通過new來直接調用,也可以直接調用
var a = new getInstance("a");
var b = getInstance("b");
// 證明該對象僅可被實例化一次
console.log(a === b);  // true
// 證明并未創(chuàng)建一個額外的全局變量
console.log(window.instance); // undefined
console.log(a === window.instance);  // false

有些同學會想,既然這里只是不想額外創(chuàng)建一個單例對象的全局實例變量,那我干脆將整個邏輯都包裹起來,比如我們需要一個可以通過傳入html內容動態(tài)創(chuàng)建div的單例對象,只需要寫成如下形式:

var CreateDiv;
(function() {
    var instance;
    CreateDiv = function(html) {
        if (instance) {
            return instance;
        }
        this.html = html;
        this.init();
        return instance = this;
    };
    CreateDiv.prototype.init = function() {
        var div = document.createElement('div');
        div.innerHTML = this.html;
        document.body.appendChild(div);
    }
    return CreateDiv;
})();

var a = new CreateDiv('html1');
var b = new CreateDiv('html2');
// 證明該對象僅可被實例化一次
console.log(a === b);  // true
// 證明并未創(chuàng)建一個額外的全局變量
console.log(window.instance); // undefined
console.log(a === window.instance);  // false

這樣豈不是封裝性更好?可事實上是,相比于前兩種寫法,這里的代碼邏輯變得更加復雜。為了把instance封裝起來,我們使用了自執(zhí)行的匿名函數和閉包,并且在這個匿名函數中實現真正的Singleton構造方法和原型邏輯,這讓代碼的可維護性變差。

另外,CreateDiv的構造函數負責了兩件事情。1.創(chuàng)建對像和執(zhí)行初始化init方法,第二是保證只有一個對象。這違背了設計模式中的單一職責的原則。

所以,使用第二種方法,即避免了額外創(chuàng)建一個全局的實例變量,又能夠很好地區(qū)分開函數的職責。這種方法又叫做代理模式比如上面通過傳入html內容動態(tài)創(chuàng)建div的單例對象。

var CreateDiv = function(html ='default 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(html );
        }
        return instance;
    } 
})();
var a = new ProxyMode("html1");
var b = new ProxyMode("html2");
console.log(a===b);// true
// 這里要注意由于只會實例化一次,所以只有第一次實例化時所傳的參數才有效
console.log(b); // CreateDiv {html: "html1"}

參考

BOOK-《JavaScript設計模式與開發(fā)實踐》 第4章
Javascript設計模式詳解
【原】常用的javascript設計模式
js設計模式
[譯] 你應了解的4種JS設計模式
深入理解javascript之設計模式
JavaScript實現單例模式
JavaScript設計模式----單例模式

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容