JavaScript单例模式:从具体实现到通用包装器

作者:木棉时间:2026-10-28 14:22:28浏览:644

本文探讨JavaScript中的单例模式实现。通过遮罩层创建案例,分析直接生成、全局变量引用及闭包封装三种方案的优劣。重点介绍利用函数作为第一类对象特性,构建通用的Singleton包装器,实现唯一实例的惰性加载与复用,并指出其局限性。

《Practical Common Lisp》作者Peter Seibel曾指出,若需依赖某种模式,往往意味着存在设计缺陷。他所指的缺陷,通常源于编程语言本身的局限性,迫使开发者寻找并总结通用的解决方案以弥补不足。

无论语言是弱类型还是强类型,静态或动态,命令式或声明式,每种语言都有其固有的优缺点。这如同牙买加运动员在短跑或拳击项目中具有优势,但在瑜伽等项目中可能表现平平。

在游戏语境中,术士和暗影牧师是优秀的辅助角色,而背着梅肯的敌法则显得格格不入。映射到编程中,静态语言实现装饰者模式可能耗时费力,而JavaScript允许随时向对象添加方法,使得装饰者模式在JS中显得不再必要。

讲解JavaScript设计模式的书籍相对较少,《Pro JavaScript Design Patterns》虽属经典,但示例较为冗长。本文结合工作实际代码,总结个人理解。如有偏差,欢迎指正。

单例模式基础

一、单例模式

单例模式旨在确保一个类仅有一个实例。然而,JavaScript是一种“无类”语言。许多文章将空对象{}视为单例,虽可勉强解释,但JS生成对象的方式多样,我们需要探讨更有意义的实现方式。

考虑一个常见需求:点击按钮时弹出遮罩层,例如web.qq.com的登录界面。

单例模式基础

生成灰色背景遮罩层的代码编写相对简单。

代码如下:

var createMask = function(){
return document.body.appendChild( document.createElement('div') );
}
$('button').click( function(){
var mask = createMask();
mask.show();
})

问题在于,遮罩层是全局唯一的。每次调用createMask都会创建新的div,虽然可以在隐藏时将其移除,但这显然不合理。

再看第二种方案:在页面初始化时预先创建该div,并用一个变量引用它。

代码如下:

var mask = document.body.appendChild( document.createElement('div') );
$('button').click( function(){
mask.show();
} )

虽然这样确保页面只创建一个遮罩层div,但另一个问题随之而来:也许我们永远不需要这个遮罩层,这将浪费一个div节点。对DOM节点的操作应尽可能吝啬。

如果能借助一个变量来判断div是否已创建,是否可行?

代码如下:

var mask;
var createMask = function(){
if ( mask ) return mask;
else{
mask = document.body.appendChild( document.createElement('div') );
return mask;
}
}

看起来不错,这里确实完成了一个产生单例对象的函数。但再仔细审视这段代码,会发现不妥之处。

首先,该函数存在副作用,其内部改变了外部变量mask的引用。在多人协作项目中,createMask是一个不安全的函数。此外,mask这个全局变量并非必需。让我们进一步改进。

代码如下:

var createMask = function(){
var mask;
return function(){
return mask || ( mask = document.body.appendChild( document.createElement('div') ) );
}
}()

通过一个简单的闭包将变量mask包裹起来,至少对于createMask函数而言,它是封闭的。

或许有人会觉得单例模式过于简单。确实,一些设计模式非常基础,即使从未关注过设计模式概念,日常代码中也不自觉地使用了某些模式。就像多年前明白“老汉推车”原理时,感叹“原来这就是老汉推车”。

GOF的23种设计模式,在软件开发中早已存在并被反复使用。如果程序员未明确意识到使用了某些模式,下次可能会错过更合适的设计(引自《松本行弘的程序世界》)。

通用单例包装器实现

回到正题,前述单例仍有缺点:它仅适用于创建遮罩层。若需编写另一个函数来创建唯一的XHR对象,能否找到一个通用的Singleton包装器?

在JavaScript中,函数是第一类对象,意味着函数可作为参数传递。查看最终代码。

代码如下:

var singleton = function( fn ){
var result;
return function(){
return result || ( result = fn.apply( this, arguments ) );
}
}
var createMask = singleton( function(){
return document.body.appendChild( document.createElement('div') );
})

使用一个变量保存第一次的返回值,若已赋值,后续调用优先返回该变量。真正创建遮罩层的代码通过回调函数传入Singleton包装器。这种方式实际上类似于桥接模式,关于桥接模式,将在稍后讨论。

然而,Singleton函数并非完美,它仍需一个变量result来寄存div的引用。遗憾的是,JavaScript的函数式特性尚不足以完全消除声明和语句。

作者最新文章