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設計模式----單例模式