1.实例演进
考虑实现如下功能,点击一个按钮后出现一个遮罩层。
原始办法:我们只需要实现一个创建遮罩层的函数并将其作为按钮点击的回调事件即可。如下:
createMask
这里我们来看看效果:
可以看到,每次点击都会创建一个新的遮罩层。而且老的遮罩层也仍然存在。这会无限增大html的体积。
改进办法1:将每次点击遮罩层隐藏改为将其移除。即:
mask.addEventListener('click', function () {
document.body.removeChild(this);
});
具体效果这里就不演示了。
但即使这样,我们每一次点击仍然会创建一个新的遮罩层,损耗性能。
改进办法2:在页面初始化时建立一个隐藏的遮罩,每次点击只是控制其display属性。
createMask
这样的话就不用每次点击按钮都新创建一个遮罩层了,可是还有一个缺点,那就是,如果用户并没有点击按钮,这个遮罩层不是白白创建了吗。
改进办法3:点击按钮的时候,动态判断是否需要新建一个遮罩层
createMask
这样看上去已经很不错了,可是问题还是有,那就是mask成为了一个全局变量。
改进办法4:将mask当做局部变量,createMask当做闭包来引用。
createMask
到这里,我们的代码已经很不错了。然而,设想这样一个场景,你在不同的页面,需要使用不同背景颜色的mask。怎么办?一个简单的想法,就是像createMask里面传参。可是,你又有了新的需求,不同页面还需要不同的透明度,也简单,再增加一个参数。那么问题来了,第一,你不可能无限制地为函数增加参数,第二,你的两个页面需要创建的mask可能是根本不一样的,比如另一个mask是一张图片,和前一种mask的创建方法没有什么共同性。那么这里最好的办法其实就是定义不同的创建mask的方法,然后根据需要使用和不同的创建方法。
改进办法5:抽象成更通用的单例模式
createMask
但是这里,为了使用 createMask的时候可以动态传参,我引入了一个全局变量。不知道有没有同学知道这里该如何不引入全局变量且能支持传参呢?如果知道的同学,还请不吝赐教哈
(找到办法了,写这篇文章的时候我还没有看到《JavaScript设计模式与开发实践》这本书,看过以后,发现这一章和作者的思路还是挺接近的,但是作者的分析更加全面和精辟。而且,作者也没有通过引入全局变量来进行抽象,建议大家看一下这本书。真的很精辟。强烈推荐。)
改进办法6:利用闭包抽象成更通用的单例模式
createMask
2. 单例模式的思想与优点
由第1节的遮罩层例子,引出单例模式的设计思想,其实质就是:保证一个类仅有一个实例,并且提供一个访问它的全局访问点。
单体模式具有如下优点:
- 可以用来划分命名空间,减少全局变量的数量。
- 使用单体模式可以使代码组织的更为一致,使代码容易阅读和维护。
- 可以被实例化,且实例化一次。
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
// 证明创建了一个额外的全局变量
console.log(window.instance); // Singleton {name: "a", instance: null}
console.log(a === window.instance); // true
这种模式很好理解,但是额外创建了一个全局变量。
闭包实现单例模式
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
// 证明并未创建一个额外的全局变量
console.log(window.instance); // undefined
console.log(a === window.instance); // false
有些同学会想,既然这里只是不想额外创建一个单例对象的全局实例变量,那我干脆将整个逻辑都包裹起来,比如我们需要一个可以通过传入html内容动态创建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
// 证明并未创建一个额外的全局变量
console.log(window.instance); // undefined
console.log(a === window.instance); // false
这样岂不是封装性更好?可事实上是,相比于前两种写法,这里的代码逻辑变得更加复杂。为了把instance封装起来,我们使用了自执行的匿名函数和闭包,并且在这个匿名函数中实现真正的Singleton构造方法和原型逻辑,这让代码的可维护性变差。
另外,CreateDiv的构造函数负责了两件事情。1.创建对像和执行初始化init方法,第二是保证只有一个对象。这违背了设计模式中的单一职责的原则。
所以,使用第二种方法,即避免了额外创建一个全局的实例变量,又能够很好地区分开函数的职责。这种方法又叫做代理模式比如上面通过传入html内容动态创建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设计模式与开发实践》 第4章
Javascript设计模式详解
【原】常用的javascript设计模式
js设计模式
[译] 你应了解的4种JS设计模式
深入理解javascript之设计模式
JavaScript实现单例模式
JavaScript设计模式----单例模式