本文总结了JavaScript中实现单例模式的多种技巧,包括使用全局变量、闭包封装、构造函数静态属性缓存以及重写构造函数等方法。重点分析了各方法的优缺点,特别是构造函数重写导致的原型链断裂问题及其修复方案,并介绍了惰性加载和分支单例在复杂场景下的应用。
使用全局变量保存单例
一、使用全局变量保存单例
这是最简单的实现方法
function Person(){
this.createTime=new Date();
}
var instance=new Person();
function getInstance(){
return instance;
}
在加载该JS文件时立即创建一个Person对象,并将其保存到名为instance的全局变量中。后续每次使用时直接获取该对象即可。如果整个运行周期内从未调用过该对象,则会造成内存浪费,因此我们可以对这种实现方式进行优化。
var instance
function getInstance(){
if(!instance){
instance=new Person();
}
return instance;
}
通过这种方式,对象仅在第一次被使用时才会被创建。 然而,这种方法存在一个明显的缺点:由于instance是全局变量,在多人协作或长期开发的项目中,很难确保该变量不会被其他代码意外修改或覆盖。这可能导致在调用时,instance指向的并非预期的Person对象。 为了解决这个问题,我们可以考虑使用闭包将instance封装起来,使其不再暴露在全局作用域中。
闭包创建对象
二、闭包创建对象
var getInstance(){
var instance;
return function(){
if(!instance){
instance=new Person();
}
return instance;
}
}();
通过上述方式,instance变量被封装在闭包内部,从而避免了被外部代码修改的风险。 现在,可以通过getInstance()函数来获取单例实例。但这里出现了一个新问题:如果用户直接使用new Person()来创建对象,仍然会生成多个不同的实例。由于JavaScript无法像Java那样将构造函数私有化,我们需要找到一种方法,使得无论调用多少次new Person(),返回的都是同一个实例。
构造函数的静态属性缓存实例
三、构造函数的静态属性缓存实例
先看代码
function Person(){
//如果已经缓存了实例,则直接返回缓存的实例
if(typeof Person.instance==='object'){
return Person.instance;
}
this.createTime=new Date();
//缓存实例
Person.instance=this;
return this;
}
从代码逻辑可以看出,当第一次执行new操作时,if条件判断返回false,程序继续向下执行,完成对象初始化,并将当前实例保存到Person.instance这个静态属性中。 当第二次执行new操作时,if条件判断返回true,程序直接返回已缓存的Person.instance,不再执行后续的初始化代码。因此,无论执行多少次new操作,返回的都是第一次创建的那个对象。
这个方法的缺点与方法一类似:Person.instance是一个公开属性,外部代码仍有可能对其进行修改,从而破坏单例的完整性。
我们可以参考方法二的思路,利用闭包来封装这个静态属性,或许就能解决该问题。
重写构造函数
四、重写构造函数
这种方法需要结合闭包使用,但实现逻辑比方法二更为复杂,核心在于重写构造函数。
function Person(){
//缓存实例
var instance=this;
this.createTime=new Date();
//重写构造函数
Person=function(){
return instance;
}
}
当第一次执行new操作时,程序会先调用原始构造函数来缓存当前实例,然后进行初始化操作,同时立即重写Person构造函数。此后,任何新的new操作都无法再调用到原始构造函数,只能调用到被重写后的构造函数,而该函数总是返回之前缓存的instance。 上述方法看似完美,但通过以下测试可以发现潜在问题。
//向原型添加属性
Person.prototype.prop1=true;
var p1=new Person();
//在创建初始化对象后,再次向该原型添加属性
Person.prototype.prop2=true;
var p2=new Person();
//开始测试
console.log(p1.prop1);//结果为true
console.log(p2.prop1);//结果为true
console.log(p1.prop2);//结果为undefined
console.log(p2.prop2);//结果为undefined
console.log(p1.constructor===Person);//结果为false
console.log(p2.constructor===Person);//结果为false
我们预期的测试结果应该全都是true。 下面对测试代码进行分析。
Person.prototype.prop1=true; 这行代码是在原始构造函数的原型上增加了prop1属性,并赋值为true。
而在执行 var p1=new Person(); 之后,Person构造函数已经被重写。
因此,Person.prototype.prop2=true; 这行代码实际上是在新的原型(即重写后的构造函数原型)上增加了prop2属性。
当执行 var p2=new Person(); 时,p2和p1实际上是同一个对象,即由原始构造函数创建的那个缓存实例。
因此,p1和p2都拥有prop1属性,但不包含prop2属性。
同样地,p1和p2的constructor属性指向的是原始构造函数,而此时的Person变量已经指向了新的重写函数。
为了达到预期的运行结果,我们需要对代码进行一些修改。
function Person(){
//缓存实例
var instance=this;
//重写构造函数
Person=function(){
return instance;
}
//保留原型属性
Person.prototype=this;
//实例
instance=new Person();
//重置构造函数引用
instance.constructor=Person;
//其他初始化
instance.createTime=new Date();
return instance;
}
再次运行前面的测试代码,结果将全部为true。
惰性加载与分支单例
五、惰性加载: 在大型或复杂的项目中,这种方法能起到优化作用:那些开销较大但很少使用的组件可以被包装到惰性加载的单例中,示例程序:
/* Singleton with Private Members, step 3. */
MyNamespace.Singleton = (function() {
// Private members.
var privateAttribute1 = false;
var privateAttribute2 = [1, 2, 3];
function privateMethod1() {
...
}
function privateMethod2(args) {
...
}
return { // Public members.
publicAttribute1: true,
publicAttribute2: 10,
publicMethod1: function() {
...
},
publicMethod2: function(args) {
...
}
};
})();
/* General skeleton for a lazy loading singleton, step 1. */
MyNamespace.Singleton = (function() {
function constructor() { // All of the normal singleton code goes here.
// Private members.
var privateAttribute1 = false;
var privateAttribute2 = [1, 2, 3];
function privateMethod1() {
...
}
function privateMethod2(args) {
...
}
return { // Public members.
publicAttribute1: true,
publicAttribute2: 10,
publicMethod1: function() {
...
},
publicMethod2: function(args) {
...
}
}
}
})();
/* General skeleton for a lazy loading singleton, step 2. */
MyNamespace.Singleton = (function() {
function constructor() { // All of the normal singleton code goes here.
...
}
return {
getInstance: function() {
// Control code goes here.
}
}
})();
/* General skeleton for a lazy loading singleton, step 3. */
MyNamespace.Singleton = (function() {
var uniqueInstance; // Private attribute that holds the single instance.
function constructor() { // All of the normal singleton code goes here.
...
}
return {
getInstance: function() {
if(!uniqueInstance) { // Instantiate only if the instance doesn't exist.
uniqueInstance = constructor();
}
return uniqueInstance;
}
}
})();
六、使用分支单例: 针对特定运行环境的代码可以被包装到分支型单例中,示例程序:
/* SimpleXhrFactory singleton, step 1. */
var SimpleXhrFactory = (function() {
// The three branches.
var standard = {
createXhrObject: function() {
return new XMLHttpRequest();
}
};
var activeXNew = {
createXhrObject: function() {
return new ActiveXObject('Msxml2.XMLHTTP');
}
};
var activeXOld = {
createXhrObject: function() {
return new ActiveXObject('Microsoft.XMLHTTP');
}
};
})();
/* SimpleXhrFactory singleton, step 2. */
var SimpleXhrFactory = (function() {
// The three branches.
var standard = {
createXhrObject: function() {
return new XMLHttpRequest();
}
};
var activeXNew = {
createXhrObject: function() {
return new ActiveXObject('Msxml2.XMLHTTP');
}
};
var activeXOld = {
createXhrObject: function() {
return new ActiveXObject('Microsoft.XMLHTTP');
}
};
// To assign the branch, try each method; return whatever doesn't fail.
var testObject;
try {
testObject = standard.createXhrObject();
return standard; // Return this if no error was thrown.
}
catch(e) {
try {
testObject = activeXNew.createXhrObject();
return activeXNew; // Return this if no error was thrown.
}
catch(e) {
try {
testObject = activeXOld.createXhrObject();
return activeXOld; // Return this if no error was thrown.
}
catch(e) {
throw new Error('No XHR object found in this environment.');
}
}
}
})();