本文深入解析JavaScript中单体模式的多种实现方式。从传统面向对象概念对比入手,逐步演示如何通过对象字面量、命名空间隔离、闭包模拟私有成员,最终实现基于惰性加载的高性能单体模式。文章涵盖代码演进过程,帮助开发者理解不同场景下的最佳实践。
JavaScript单体模式概述
单体模式作为一种软件开发模式在多种面向对象语言中得到了广泛应用。在JavaScript中,单体模式的使用也非常普遍。然而,由于JavaScript拥有独特的面向对象机制,尽管其单体模式的思想与传统面向对象语言一致,但在具体实现上仍存在差异。
传统面向对象语言中,单体模式定义为只能被实例化一次,并通过一个众所周知的访问点访问的类。该定义突出了传统面向对象语言的两个特征:类和实例化。因此,传统单体模式基于类的自然特性,即使用class关键字定义类,通过new关键字实例化,但需保证每次new操作返回的是同一个实例,或仅允许调用一次构造函数。
JavaScript中单体模式的定义侧重于将一个划分命名空间并将一批相关方法和属性组织在一起的对象。若该对象可被实例化,则只能实例化一次。与传统定义不同,JavaScript将单体实质定义为对象而非类,体现了其基于对象的特性。此外,若支持实例化(使用new关键字),则存在多种创建单体对象的方式。然而,这种方式并非JavaScript的自然特征,因为通过new创建的对象实际上是通过function模拟构造函数实现的(尽管ES6支持class关键字,但尚未得到浏览器广泛支持)。那么,如何利用JavaScript的自然特征来实现单体模式呢?
var Singleton={
attribute1:true,
attribute2:10,
method1:function(){
},
method2:function(arg){
}
}
基础实现与命名空间隔离
此处定义了一个对象Singleton,内部包含若干属性和方法。将其包含在页面中,JS载入时即创建该对象。调用时使用Singleton.method1,其实例化随页面载入JS解析执行过程完成,并未使用new关键字。这是JavaScript实现单体模式与传统面向对象语言的一大不同,这种方式更为简单易懂。但存在若干缺点,最明显的是未提供命名空间。若其他程序员在页面中定义同名Singleton变量,易改写和混淆该单体对象。因此,针对此问题,进行如下改写:
var mySpace={};
mySpace.Singleton={
attribute1:true,
attribute2:10,
method1:function(){
},
method2:function(arg){
}
}
首先定义一个mySpace命名空间,然后将单体对象Singleton挂载于其下。这大大降低了与其他程序员冲突及误操作的可能性。即使其他人在全局作用域定义Singleton变量,也不会污染此单体对象,从而实现了前文所述划分命名空间并将相关属性和方法组织在一起的功能。
利用闭包模拟私有成员
此方法仍存在缺点:单体对象的所有属性和方法均为共有,外部可随时访问和修改。因此,采用闭包来模拟私有属性和方法,如下所示:
mySpace.Singleton=(function(){
var privateAttribute1=false;
var privateAttribute1=[1,2,3];
function privateMethod1(){
}
function privateMethod2(){
}
return {
publicAttribute1:true,
publicAttribute2:10,
publicMethod1:function(){
privateAttribute1=true;
privateMethod1();
},
publicMethod2:function(arg){
privateAttribute1=[4,5,6];
privateMethod2();
}
}
})();
此处直接给该单体对象赋值了一个匿名自执行函数。在该函数中,使用var和function关键字分别定义其私有属性和方法。这些在函数外部(即单体对象外部)无法直接访问,因为函数执行完毕后,其内部作用域空间会被回收。这正是利用闭包模拟私有属性和方法的原因。在该函数(闭包)中,最终返回一个对象,包含一些公有方法和属性,外部可直接调用。同时,这些公有方法因定义在函数内部,可调用其私有属性和方法。但外界只能通过返回的公有方法和属性完成某些操作,无法直接调用Singleton.privateMethod1等属性。这使得该单体对象既隔离了外界对私有属性和方法的直接访问,又提供给外界一些共有属性和方法以完成特定操作。
惰性加载优化方案
这种由匿名函数自执行构造的单体模式在诸多JS库中被广泛使用,但仍存在一个问题:若在载入页面时并不需要用到该对象,且该对象的创建较为耗费开销(如需进行大量计算或多次访问DOM树及其属性等),合理的做法是按需创建,而非随JS解析执行直接创建。这种概念被称为惰性加载(lazy loading)。于是对以上代码进行修改:
mySpace.Singleton=(function(){
var uniqueInstance;
function constructor(){
var privateAttribute1=false;
var privateAttribute1=[1,2,3];
function privateMethod1(){
}
function privateMethod2(){
}
return {
publicAttribute1:true,
publicAttribute2:10,
publicMethod1:function(){
privateAttribute1=true;
privateMethod1();
},
publicMethod2:function(arg){
privateAttribute1=[4,5,6];
privateMethod2();
}
}
}
return {
getInstance:function(){
if(!uniqueInstance){
uniqueInstance=constructor();
}
return uniqueInstance;
}
}
})();
首先在匿名函数中定义一个私有变量uniqueInstance,作为判断单体对象是否被创建的句柄。然后将此前所有对单体对象定义的属性和方法置于一个名为constructor的函数中。仅当该函数被调用时,才会创造该单体对象,否则不直接创建。随后,返回一个包含getInstance方法的对象,该方法供外部调用。调用该方法时,首先判断单体对象是否存在,若存在则直接返回,否则调用constructor函数构造该单体对象再返回。最后,若需调用该单体对象的某个方法,需使用mySpace.Singleton.getInstance().publicMethod1()。如此调用时才会创建该单体对象,否则该单体对象不会被自动创建,从而实现了按需加载或惰性加载。