保证一个类只有一个实例,并提供一个访问它的全局访问点,这就是单例模式。
单例模式是一种常用的模式,有一些对象我们往往只需要一个,比如window
对象、全局缓存等。
根据单例模式的特点,在实现单例模式时,我们要保证某个类只能创建一个实例对象,那么实现的基本思路就是:用一个变量标识当前是都已经为某个类创建过对象,如果是,在下一次获取该类的实例时,直接返回之前创建的对象,否则创建一个新对象,示例代码如下:
// 定义一个构造函数MyExample,定义属性name与instance
function MyExample(name) {
this.name = name;
this.instance = null;
}
// 在原型上定义getName方法
MyExample.prototype.getName = function () {
console.log(this.name);
};
// 为构造函数定义getInstance方法,用来获取类的实例
MyExample.getInstance = function (name) {
// 标识是否创建了实例,如果没有,赋值为新的实例,如果已经创建,返回原来的实例
if (!this.instance) {
this.instance = new MyExample(name);
}
return this.instance;
};
// 为MyExample类创建实例,看似创建两个实例,实际上都是同一个实例
var e1 = MyExample.getInstance("example1");
var e2 = MyExample.getInstance("example2");
console.log(e1 === e2); // true
我们通过MyExample.getInstance
来获取 MyExample
类的唯一对象,这种方式相对简单,但有一个问题,就是增加了这个类的“不透明性”,MyExample
类的使用者必须知道这是一个单例类,跟以往通过new XXX
的方式来获取对象不同。
在上一步中,由于该类的“不透明性”,用户在使用时会有一些困难,因此在本节中实现一个“透明”的单例类,让用户在使用这个类创建对象时,可以和使用其他普通类一样。
接下来实现一个单例类CreateDiv
,这个类的作用是在页面创建唯一的div
节点,代码如下:
var CreateDiv = (function () {
var instance; // 标识是否创建过实例
// 创建CreateDiv类
var CreateDiv = function (html) {
// 如果已经创建过实例,返回当前实例
if (instance) {
return instance;
}
// 如果没有创建实例。创建一个div元素,并返回
this.init();
this.html = html;
return (instance = this);
};
// 定义init方法,目的是创建一个div元素,元素内容由我们自己定义
CreateDiv.prototype.init = function () {
var div = document.createElement("div");
div.innerHTML = this.html;
document.body.appendChild(div);
};
return CreateDiv;
})();
var e1 = new CreateDiv("example1");
var e2 = new CreateDiv("example2");
console.log(e1 === e2); // true
虽然现在完成了一个透明的单例类的编写,但它同样有一些缺点,为了把instance
封装起来,使用了自执行的匿名函数和闭包,并且让这个匿名函数返回真正的构造方法,复杂度高,可读性也变差了。
在上面的例子中,假设我们某天需要利用这个类,在页面中创建千千万万的div
,即要让这个类从单例类变成一个普通的可产生多个实例的类,那我们必须得改写CreateDiv
构造函数,这种修改会给我们带来不必要的烦恼,因此我们通过引入代理类的方式来解决这个问题
首先在CreateDiv
构造函数中,把负责管理单例的代码提出来,使它成为一个普通的创建div
的类:
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 e1 = new ProxySingletonCreateDiv("example1");
var e2 = new ProxySingletonCreateDiv("example2");
console.log(e1 === e2); // true
上面的例子通过引入代理类的方式完成了一个单例模式的改写,与之前不同的是,现在把负责管理单例的逻辑移到了代理类proxySingletonCreateDiv
中。这样一来,CreateDiv
就变成了一个普通的类,它跟 proxySingletonCreateDiv
组合起来可以达到单例模式的效果。
前面提到的几种单例模式的实现,更多的是接近传统面向对象语言中的实现,单例对象从“类”中创建而来,在以类为中心的语言中,这是很自然的做法。比如在Java
中,如果需要某个对象,就必须先定义一个类,对象总是从类中创建而来的。
但JavaScript
其实是一门无类语言,创建对象的方法非常简单,既然我们只需要一个“唯一”的对象,为什么要为它先创建一个“类”呢,传统的单例模式实现在JavaScript
中并不适用。
单例模式的核心是确保只有一个实例,并提供全局访问。
全局变量不是单例模式,但在JavaScript
开发中,我们经常会把全局变量当成单例来使用。例如:
var a = {};
当用这种方式创建对象a
时,对象a
确实是独一无二的。如果a
变量被声明在全局作用域下,我们可以在代码中的任何位置使用这个变量,这样就满足了单例模式的两个条件。但是全局变量存在很多问题,它很容易造成命名空间污染。
我们可以采用以下几种方式降低全局变量带来的命名污染:
1、使用命名空间,适当地使用命名空间,并不会杜绝全局变量,但可以减少全局变量的数量,最简单的方法依然是用对象字面量的方式:
var namespace1 = {
a: function () {
alert(1);
},
b: function () {
alert(2);
},
};
2、使用闭包封装私有变量,这种方法把一些变量封装在闭包的内部,只暴露一些接口跟外界通信:
var user = (function () {
var __name = "sven",
__age = 29;
return {
getUserInfo: function () {
return __name + "-" + __age;
},
};
})();
我们用下划线来约定私有变量__name
和__age
,它们被封装在闭包产生的作用域中,外部是访问不到这两个变量的,这就避免了对全局的命令污染。
惰性单例指的是在需要的时候才创建对象实例。
惰性单例是单例模式的重点,实例对象instance
总是在我们调用MyExample.getInstance
的时候才被创建,而不是在页面加载好的时候就创建,示例代码如下:
MyExample.getInstance = (function () {
var instance = null;
return function (name) {
if (!instance) {
instance = new MyExample(name);
}
return instance;
};
})();