
设计模式不是"炫技",而是"沉淀"
什么是单例模式
单例模式(Singleton Pattern)
就像'独生子女',确保某个类在整个系统中只有一个实例,并提供一个全局访问点。简单来说,一个类一辈子只有一个孩子
。在 JavaScript
编程世界中,例如 Web
应用中的登录窗口,无论你点击多少次登录按钮,只会弹出一次。
为什么需要单例模式
想象一下,你是DR
钻戒的老板。我们推出一款特殊的钻戒------"单例钻戒"。这款钻戒一生只能购买一次,每位顾客终身仅能拥有一枚。这正是单例模式(Singleton Pattern)
的生动写照。
常见应用场景包括
- 浏览器中的任务管理器或标签页管理:保证只有一个全局管理器在运行。
- 通知中心 / 消息中心:统一处理应用内的消息推送和响应。
- 侧边栏 / 弹窗组件:页面中始终只显示一个实例,避免重复创建。
- 全局配置文件管理:读取并维护一份全局配置,供整个应用使用。
- 如
Vue
的Vuex
,React
的Context
或Redux
)和组件生命周期管理机制。
通过单例模式,我们可以有效地控制资源的使用,减少重复对象的创建,提升系统性能与稳定性。
单例模式实现
透明单例模式实现
javascript
var Singleton = function (name) {
this.name = name;
};
Singleton.prototype.getName = function () {
alert(this.name);
};
Singleton.getInstance = (function () {
var instance = null;
return function (name) {
if (!instance) {
instance = new Singleton(name);
}
return instance;
};
})();
var a = Singleton.getInstance( 'sven1' );
var b = Singleton.getInstance( 'sven2' );
alert ( a === b ); // true
上面的代码中是一个简单实现单例模式的方式,通过调用
Singleton.getInstance()
来获取Singketon
类的唯一实例,实现很简单,但是有一个缺点,降低了类的"透明性"
。 通常情况下,使用一个类的时候,习惯用new ClassName()
的方式来新建一个对象,但是Singleton
类并不是通过new
直接创建实例的,而是通过Singleton.getInstance()
这个静态方法来获取唯一的实力,这就必须要求使用这个类的人必须知道它是一个单例模式,并且要记住不能用new来创建对象,必须使用Singleton.getInstance()
来创建对象,这种写法会让类的使用变得不够直观和不透明,增加了使用上的认知负担,很容易导致吴用。
不透明单例模式
javascript
var CreateDiv = (function () {
var instance;
var 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("sven1");
var b = new CreateDiv("sven2");
console.log("a:", a);
console.log("b:", b);
console.log(a === b);
在上一步骤中,我们实现了一个不透明的单例模式,即用户必须通过
Singleton.getInstance()
的方式来获取实例,而不是使用常见的 new
方式创建对象。这种方式虽然实现了单例的效果,但不够直观,降低了类的"透明性"
。
为了改进这一点,我们可以尝试实现一个透明
的单例模式。所谓"透明"
,是指用户在使用时可以像使用普通类一样,直接通过 new CreateDiv()
的方式来创建实例,而无需关心这是一个单例类。 我们通过一个自执行的闭包函数来实现这个目标。在这个闭包中,我们将实例变量(如 instance
)封装为"私有变量"
,使其不会被外部轻易访问或修改,并且在整个页面不刷新的情况下,始终只存在一个实例。
例如,我们定义了一个 CreateDiv
类,它在被 new
调用时会检查是否已经存在实例,如果存在,则返回已有的实例,从而保证了全局唯一性。整个过程对使用者是透明的,使用方式和普通类完全一致,这样一来,既保留了单例的核心特性,又提升了使用的直观性和代码的可维护性。
虽然很流畅,很有单例的感觉,但是透明的单例模式还是有缺点的:
- 代码复杂度提高了:为了封装单例逻辑,使用了闭包和自执行函数,让代码变得不容易理解。
- 构造函数职责不单一:
createDiv
构造函数不仅要负责创建对象和初始化,还要控制只能有一个单例的逻辑,违反了"单一职责原则"- 不容易扩散或者修改:如果我们还想把这个类从
"只能创建一个实例"
改为"可以创建多个实例"
,就必须去修改构造函数内部的逻辑,非常不灵活,虽然这个写法让使用更自然(像普通类一样用 new 创建实例),但实现起来更复杂,维护成本也更高
用代理实现单例模式
javascript
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 ProxySingletonCreateDiv = (function () {
var instance;
return function (html) {
if (!instance) {
instance = new CreateDiv(html);
}
return instance;
};
})();
var a = new ProxySingletonCreateDiv("sven1");
var b = new ProxySingletonCreateDiv("sven2");
console.log("a", a);
console.log("b", b);
console.log(a === b);
上面代码对
透明
单例模式的代理模式进行了优化,原来的CreateDiv
类在构造函数中直接包含了控制单例的逻辑导致职责不清楚,不容易扩展,现在通过引入一个代理类ProxySingletonCreateDiv
,将管理单例的逻辑从CreateDiv
中抽离出去,让它回归为一个存粹用于创建div
元素的普通类,具体来说,CreateDiv
只负责创建对象和初始化操作,比如生成div
并插入页面,单例的逻辑则有代理函数ProxySingletonCreateDiv
来处理,这个代理函数是一个闭包,内部维护了一个唯一的实例,当第一次调用的时候会创建CreativeDiv
实力,之后直接返回该实例,这种方法是缓存代理的一种典型应用,让CreateDiv
更加的灵活,通用,同时又不丢失单例的特性。
javaScript中的单例模式
上面的集中单例模式实现方式偏向于传统的面向对象的写法,即通过"类"来创建唯一的对象,但是在javaScript
中,这种做法也不完全适用,javaScript
在es5
中是一门无类(class-free)
语言,创建对象非常简单,如果只需要一个唯一的对象,没必要先定义一个类再来创建实例,要知道单例模式的核心即可:"确保只有一个实例,并提供全局访问"
,最然全局变量具备唯一
和全局访问
的特性,但是它并不是真正的单例模式,而且存在命名冲突,全局作用域污染,在大型的项目中使用全局作用域尤其危险,为了减少全局变量污染带来的问题,可以采取使用命名空间和使用闭包封装私有变量解决。
使用命名空间
适当的使用命名空间,并不会杜绝全局变量,但是可以减少全局变量的数量,最简单的方法就是对象的字面量的方式解决:
javascript
var namespace1 = {
a: function () {
alert(1);
},
b: function () {
alert(2);
},
};
把 a
和 b
都定义为 namespace1
的属性,这样可以减少变量和全局作用域打交道的机会。
使用闭包封装私有变量
javascript
var user = (function () {
var __name = "sven",
__age = 29;
return {
getUserInfo: function () {
return __name + "-" + __age;
},
};
})();
将一些变量封装在闭包的内部,只暴露一些接口跟外界通信,用下划线来约定私用变量__name
和__age
,它们被封装在闭包产生的作用域中,外部变量访问不到这两个变量,这就进行了全局的隔绝污染。
惰性单例
前面了解了单例模式的一些实现方法,接下来学习一下单例模式比较重要的一个知识"惰性单例【Lazy Singleton
】",惰性单例指的是需要的时候才创建实例,而不是在页面加载的时候就提前创建,这种方式更节省资源,实际开发中,以弹窗为例,如果一开始就创建弹窗元素,用户可能根本不需要弹出窗口,这样回造成资源浪费,如果每次点击按钮都创建一个新的弹窗,虽然实现了按需展示,但却失去了单例的唯一性,正确的做法是首次点击创建弹窗,再次点击直接使用已经创建好的弹窗实例,这样就实现了惰性单例,即实现了只创建一次,又坐到了用的时候才创建,兼顾了功能与性能。
javascript
<html>
<head>
<style>
.modal {
position: fixed;
width: 200px;
height: 200px;
line-height: 200px;
text-align: center;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
background: goldenrod;
}
</style>
</head>
<body>
<button id="open">打开</button>
<button id="close">关闭</button>
<script>
const Moda = (function () {
let instance = null;
return function () {
if (!instance) {
instance = document.createElement("div");
instance.innerHTML = "模态框";
instance.className = "modal";
instance.style.display = "none";
document.body.appendChild(instance);
}
return instance;
};
})();
document.getElementById("open").onclick = function () {
// 创建一个模态框
const modal = Moda();
modal.style.display = "block";
};
document.getElementById("close").onclick = function () {
// 删除模态框
const modal = Moda();
modal.style.display = "none";
};
</script>
</body>
</html>

通用的惰性单例
我们知道了什么是惰性单例和如何实现惰性单例,但是不难发现,普通的惰性单例还是违反了单一职责
原则,创建对象和管理单例的逻辑都放在了Moda
内部,如果我们下次需要创建页面唯一的iframe
或者script
或者更多类型的标签,就必须如法炮制创建更多的Moda
函数,几乎照抄一遍,针对这种方法需要将不变的部分隔离出来,创建div
,iframe
,img
逻辑其实可以完全抽象出来【使用一个变量标志是否创建过,如果创建过则下次直接返回这个已经常见好的对象】。
javascript
return function () {
if (!instance) {
instance = document.createElement("div");
...
}
return instance;
};
接下来就把如何管理单例的逻辑抽离出来,这些逻辑封装在getSingle函数内部,创建对象的方法fn被当成参数动态传入getSingle函数。
javascript
var getSingle = function( fn ){
var result;
return function(){
return result || ( result = fn .apply(this, arguments ) );
}
};
接下来将用于创建登录浮窗的方法用参数 fn
的形式传入 getSingle
,我们不仅可以传入Mode
,还能传入 ModeScript
、ModeIframe
、ModeXhr
等。之后再让getSingle
返回一个新的函数,并且用一个变量 result
来保存fn
的计算结果。result
变量因为身在闭包中,它永远不会被销毁。在将来的请求,如果 result 已经被赋值,那么它将返回这个值。
javascript
<html>
<head>
<style>
.modal {
position: fixed;
width: 200px;
height: 200px;
line-height: 200px;
text-align: center;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
background: goldenrod;
}
</style>
</head>
<body>
<button id="open">打开</button>
<button id="close">关闭</button>
<script>
let getSingle = function (fn) {
let result;
return function () {
return result || (result = fn.apply(this, arguments));
};
};
let ModelDiv = function () {
let div = document.createElement("div");
div.innerHTML = "模态框";
div.className = "modal";
div.style.display = "none";
document.body.appendChild(div);
return div;
};
let ModelIframe = function () {
let iframe = document.createElement("iframe");
iframe.style.display = "none";
document.body.appendChild(iframe);
return iframe;
};
var createSingleLoginLayer = getSingle(ModelDiv);
var createSingleLoginLayer2 = getSingle(ModelIframe);
document.getElementById("open").onclick = function () {
// 创建一个模态框
const modal = createSingleLoginLayer();
const ModelIframe1 = createSingleLoginLayer2();
modal.style.display = "block";
ModelIframe1.style.display = "block";
};
document.getElementById("close").onclick = function () {
// 删除模态框
const modal = createSingleLoginLayer();
const ModelIframe2 = createSingleLoginLayer2();
modal.style.display = "none";
ModelIframe2.style.display = "none";
};
</script>
</body>
</html>

结语
设计模式不是"炫技",而是"沉淀",希望我们通过阅读和学习《JavaScript设计模式》和实践中,在显示业务需求开发中写出更具有可维护性,可扩展性的代码。
致敬------ 《JavaScript设计模式》· 曾探