JavaScript模式——单例模式:别再乱new了,一个实例就够了!
你有没有遇到过这样的场景:页面上的登录浮窗,用户关掉再点,结果又弹出一个新的——两个一模一样的窗口傻傻分不清?或者你辛辛苦苦写了个全局配置对象,一不小心被覆盖得面目全非?这些让人抓狂的问题,单例模式都能帮你解决。
1. 什么是单例模式?
单例模式的定义简单粗暴:保证一个类只有一个实例,并提供一个全局的访问点。
想象一下:你家的电视机遥控器,一个就够了,不需要第二个;公司的打印机,大家共用一台,没人想多买一台占地方。在程序里,线程池、全局缓存、浏览器里的window对象,都是单例的典型代表。
最常见的例子就是登录浮窗——你点一万次登录按钮,那个浮窗也只被创建一次,而不是每次都弹出个“克隆体”。这就是单例模式的魅力。
2. 传统实现:一个变量搞定
最简单的方式:用一个变量标记是否已经创建过对象,如果创建过,下次直接返回它。
var Singleton = function(name) {
this.name = name;
this.instance = null; // 这个属性用来存实例?不,放这里不合适
};
// 把实例挂在构造函数上(作为静态属性)
Singleton.getInstance = function(name) {
if (!this.instance) {
this.instance = new Singleton(name);
}
return this.instance;
};
var a = Singleton.getInstance('sven1');
var b = Singleton.getInstance('sven2');
console.log(a === b); // true
或者用闭包把实例藏起来:
var Singleton = function(name) {
this.name = name;
};
Singleton.prototype.getName = function() {
return this.name;
};
Singleton.getInstance = (function() {
var instance = null;
return function(name) {
if (!instance) {
instance = new Singleton(name);
}
return instance;
}
})();
但是这种方式有一个缺点:使用者必须知道这是一个单例类,不能用new,只能用Singleton.getInstance来获取对象,增加了“不透明性”。
3. 透明单例:像普通类一样使用
我们希望像平时一样用new来创建对象,但得到的永远是同一个实例。怎么做?可以用一个立即执行函数把真正的构造函数包起来,在里面用闭包保存实例。
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 === b); // true
你们看出来这个构造函数的问题了吗:它既要创建对象、执行初始化,又要保证只有一个实例——违反了单一职责原则。如果哪天不需要单例了,要把它改成一个可以创建多个实例的类,就得修改构造函数内部,很麻烦。
4. 代理单例:把管理单例的职责分离
更好的办法是把管理单例的逻辑单独抽到一个代理类中。这样本体类就只是一个普通的类,代理类负责单例控制。
我们先写一个普通的CreateDiv类:
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);
};
然后引入代理类ProxySingletonCreateDiv:
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 === b); // true
这样就很完美了!现在CreateDiv是一个普通类,和代理组合起来才具备单例效果。职责分明,各管各的。
5. JavaScript中的单例模式:没那么复杂
前面的例子都是从“类”的角度出发的,但JavaScript其实没有真正的类,对象可以直接创建。既然我们只需要一个唯一的对象,为什么非要先定义一个“类”呢?直接创建一个对象不就行了?
var a = {};
这已经是一个独一无二的对象了。如果把它放在全局,就是全局变量,可以随处访问——这不就满足单例的两个条件了吗?
5.1 全局变量不是单例模式
虽然全局变量可以当成单例用,但它也有命名污染和被意外覆盖的风险。Douglas Crockford甚至称全局变量是JavaScript中最糟糕的特性。所以我们得想办法减少全局变量的数量。
5.2 用命名空间减少污染
用对象字面量把相关属性和方法包起来:
var namespace = {
a: function() { alert(1); },
b: function() { alert(2); }
};
还可以动态创建命名空间:
var MyApp = {};
MyApp.namespace = function(name) {
var parts = name.split('.');
var current = MyApp;
for (var i in parts) {
if (!current[parts[i]]) {
current[parts[i]] = {};
}
current = current[parts[i]];
}
};
MyApp.namespace('event');
MyApp.namespace('dom.style');
console.dir(MyApp); // 现在 MyApp 下面有了 event 和 dom.style
5.3 用闭包封装私有变量
把一些变量藏在闭包里,只暴露接口:
var user = (function() {
var name = 'sven', age = 29;
return {
getUserInfo: function() {
return name + '·' + age;
}
};
})();
6. 惰性单例:需要时才创建
很多时候,单例对象并不需要一开始就创建。比如登录浮窗,用户可能根本不会点击登录按钮,那我们就没必要在页面加载时就创建它。
下面这个例子,只有在点击按钮时才创建浮窗,但每次点击都会创建新的——没有单例效果:
var createLoginLayer = function() {
var div = document.createElement('div');
div.innerHTML = '我是登录浮窗';
div.style.display = 'none';
document.body.appendChild(div);
return div;
};
document.getElementById('loginBtn').onclick = function() {
var loginLayer = createLoginLayer();
loginLayer.style.display = 'block';
};
改进一下,用闭包保存实例,实现惰性单例:
var createLoginLayer = (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('loginBtn').onclick = function() {
var loginLayer = createLoginLayer();
loginLayer.style.display = 'block';
};
这样既做到了惰性加载(第一次点击才创建),又保证了唯一性。
7. 通用惰性单例:把管理逻辑抽出来
上面的代码还有一点不完美:创建对象和管理单例的逻辑搅在了一起。如果以后需要创建唯一的iframe或script标签,又得把同样的单例管理代码抄一遍。
我们可以把管理单例的逻辑抽象成一个函数getSingle,它接受一个创建对象的方法,返回一个单例化的函数:
var getSingle = function(fn) {
var result;
return function() {
return result || (result = fn.apply(this, arguments));
};
};
现在创建登录浮窗的方法和单例逻辑分离了:
var createLoginLayer = function() {
var div = document.createElement('div');
div.innerHTML = '我是登录浮窗';
div.style.display = 'none';
document.body.appendChild(div);
return div;
};
var createSingleLoginLayer = getSingle(createLoginLayer);
document.getElementById('loginBtn').onclick = function() {
var loginLayer = createSingleLoginLayer();
loginLayer.style.display = 'block';
};
创建唯一的iframe也同样简单:
var createSingleIframe = getSingle(function() {
var iframe = document.createElement('iframe');
document.body.appendChild(iframe);
return iframe;
});
document.getElementById('loginBtn').onclick = function() {
var loginLayer = createSingleIframe();
loginLayer.src = 'http://baidu.com';
};
这个getSingle不仅适用于创建对象,还可以用来绑定只执行一次的事件:
var bindEvent = getSingle(function() {
document.getElementById('div1').onclick = function() {
alert('click');
};
return true;
});
var render = function() {
console.log('开始渲染列表');
bindEvent(); // 多次调用,事件也只绑定一次
};
render();
render();
render(); // 只弹一次
8. 单例模式的应用场景
-
全局缓存:比如存储用户信息、配置数据。
-
线程池、连接池:避免重复创建消耗资源。
-
登录浮窗、消息提示:页面中只应该出现一个。
-
第三方库的单例:比如
axios实例、jQuery的$本身就是一个单例。
9. 小结
单例模式是我们学习的第一个设计模式,虽然简单,但在实际开发中非常实用。通过传统的类式实现,我们了解了它的核心思想;通过JavaScript的灵活特性,我们找到了更轻量的实现方式。
特别是惰性单例和通用惰性单例的组合,把创建对象和管理单例的职责分开,既符合单一职责原则,又让代码变得优雅可复用。下次遇到需要确保唯一实例的场景,不妨试试这个模式,保证让你省心不少!
更多推荐

所有评论(0)