看本文之前需要有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('seven1');
var b = Singleton.getInstance('seven2');
alert(a === b); // true

通过Singleton.getInstance来获取Singleton类的唯一对象,这种方式相对简单,但有一个问题,就是增加了这个类的”不透明性”,Singleton类的使用者必须知道这是一个单例类,跟以往通过new xxx的方式来获取的对象不同,这里偏要使用Singleton.getInstance来获取对象。

二、透明的单例模式

用户从这个类中创建对象的时候,可以像使用其他任何普通类一样。下面,我们将使用CreateDiv单例类,它的作用是负责在页面中创建唯一的div节点。

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('seven1');
var b = new CreateDiv('seven2');

alert(a === b); // true

为了把instance封装起来,我们使用了自执行的匿名函数和闭包,并且让这个匿名函数返回真正的Singleton构造方法,这增加了一些程序的复杂度,阅读起来也不是很舒服。

CreateDiv的构造函数实际上负责了两件事情,第一是创建对象和执行初始化init方法,第二是保证只有一个对象。

三、用代理实现单例模式

var CreateDiv = function(html){
this.html = html;
this.init();
};

CreateDiv.prototype.init = function(){
var div = documents.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('seven1');
var b = new proxySingletonCreateDiv('seven2');

alert(a === b);

通过引入代理类的方式,我们同样完成了一个单例模式的编写,跟之前不同的是,现在我们负责管理单例的逻辑移到了代理类proxySingletonCreateDiv中,这样一来,CreateDiv就变成了一个普通的类,它跟proxySingletonCreateDiv组合起来可以达到单例模式的效果。

四、JavaScript中的单例模式

前面提到的几种单例模式的实现,更多的是接近传统面向对象语言中的实现,单例对象从”类”中创建而来。在以类为中心的语言中,这是很自然的做法。

但JavaScript其实是一门无类语言,也正因为如此,生搬单例模式的概念并无意义。在JavaScript中创建对象的方法非常简单,既然我们只需一个”唯一”的对象,为什么要为它先创建一个”类”呢?

单例模式的核心是确保只有一个实例,并提供全局访问

全局变量不是单例模式,但在JavaScript开发中,我们经常会把全局变量当成单例来使用:var a = {}

当用这种方式创建对象a时,对象a确实是独一无二的。如果a变量被声明在全局作用域下,则我们可以在代码中的任何位置使用这个变量,全局变量提供给全局访问是理所当然的。这样满足了单例模式的两个条件。

但是全局变量存在很多问题,它很容易造成命名空间污染。在大中型项目中,如果不加以限制和管理,程序中可能存在很多这样的变量JavaScript中的变量也很容易被不小心覆盖,相信每个JavaScript程序员都曾经历过变量冲突的痛苦,随时有可能被别人覆盖。

作为普通的开发者,我们有必要尽量减少全局变量的使用,即使需要,也要把它的污染降到最低。以下几种方式可以相对降低全局变量带来的命名污染。

  1. 使用命名空间

    适当地使用命名空间,并不会杜绝全局变量,但可以减少全局变量的数量。

    最简单的方法依然是用对象字面量的方式:

    var namespace1 = {
    a: function(){
    alert(1);
    },
    b: function(){
    alert(2);
    }
    };

    把a和b都定义为namespace1的属性,这样可以减少变量和全局作用域打交道的机会。另外,我们还可以动态地创建命名空间:

    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);

    // 上述代码等价于:

    var MyApp = {
    event: {},
    dom: {
    style: {}
    }
    };
  2. 使用闭包封装私有变量

    这种方法把一些变量封装在闭包内部,只暴露一些接口跟外界通信:

    var user = (function(){
    var __name = 'seven',
    __age = 29;

    return {
    getUserInfo: function(){
    return __name + '-' + age;
    }
    }
    })();

    用下划线来约定变量__name__age,它们被封装在闭包产生的作用域中,外部是访问不到这两个变量的,这就避免了对全局的命令污染。

五、惰性单例

惰性单例指的是在需要的时候才创建对象实例。instance实例对象总是在我们调用Singleton.getInstance的时候才被创建,而不是在页面加载好的时候就创建。

Singleton.getInstance = (function(){
var instance = null;
return function(name){
if(!instance){
instance = new Singleton(name);
}
return instance;
}
})();

不过这是基于”类”的单例模式,基于”类”的单例模式在JavaScript中并不适用。

六、小结

单例模式是一种简单但非常实用的模式,特别是惰性单例技术,在合适的时候才创建对象,并且只创建唯一的一个。更奇妙的是,创建对象和管理惰性单例的职责被分布在两个不同的方法中,这两个方法组合起来才具有单例模式的威力。