JavaScript单例模式详解:从基础实现到惰性加载与分支技术

作者:函数茶馆时间:2026-10-29 17:54:40浏览:499

本文深入解析JavaScript单例模式,涵盖基础对象字面量、闭包模块模式及惰性加载实现。重点讲解如何利用单例划分命名空间防止冲突,以及通过分支技术封装浏览器差异以优化性能。同时分析单例模式的局限性,如模块强耦合对单元测试的影响,并提供实际代码示例与最佳实践建议。

单例模式定义与基础应用场景

单例模式的核心定义在于确保一个类在整个应用程序生命周期中仅存在一个实例,并提供一个全局访问点以供外部调用。

作为一种广泛使用的编程模式,单例适用于那些只需单一实例的对象场景,例如线程池、全局缓存以及浏览器环境中的window对象。在JavaScript开发中,单例模式的应用极为普遍。以登录功能为例,当用户点击登录按钮时,页面应弹出唯一的登录浮窗。无论用户点击多少次登录按钮,该浮窗只会被创建一次,这种需求正是单例模式的典型应用场景。

1、单例模式的使用场景

在应用任何设计模式之前,明确其适用场景至关重要。尽管单例模式使用频繁,但许多开发者对其具体价值和应用场景缺乏深入理解。了解单例模式能带来哪些实际好处,有助于更合理地选择设计策略。

1).可以利用单例模式来划分命名空间,这是开发中常见的用法。

2).利用分支技术来封装不同浏览器之间的差异,这一应用相对较少见,但具有实用价值。

3).借助单例模式,可以使代码结构更加一致,从而提升代码的可读性与可维护性。

基础单例与闭包模块模式

2、最基本的单例模式

最简单的单例实现方式即使用对象字面量。这种方式将一组相关的属性和方法组织在一个对象中,形成单一访问点。


var Singleton = {
  attr1: true , 
  attr2: 10 ,
  method1 : function(){
    alert('我是方法1');
  },
  method2 : function(){
    alert('我是方法2');
  }
};

该对象是可变的,开发者可以动态添加新的属性或方法,也可以使用delete运算符移除现有成员。然而,这种可变性违背了面向对象设计的一条基本原则:类应当可扩展但不应被随意修改。若需保护某些变量不被外部修改,应将其定义在闭包内部,以实现数据隐藏。

对象字面量仅是创建单例的一种方式,且并非所有对象字面量都构成单例。那些仅用于模拟关联数组或存储数据的对象字面量,不具备单例模式的特征,不应混淆。

3、借用闭包创建单例

闭包的主要目的是保护数据,防止外部直接访问和修改内部状态。


// 命名空间
var BHX = {} ;
BHX.Singleton = (function(){
  // 添加自己的私有成员
  var a1 = true ;
  var a2 = 10 ;
  var f1 = function(){
    alert('f1');
  }
  var f2 = function(){
    alert('f2');
  }        
  // 把块级作用域里的执行结果赋值给我的单例对象
  return {
      attr1: a1 , 
      attr2: a2 ,
      method1 : function(){
        return f1();
      },
      method2 : function(){
        return f2();
      }            
  } ;
})();

alert(BHX.Singleton.attr1);
BHX.Singleton.method1();

这种基于闭包的单例实现方式也被称为模块模式。它能够将一组相关的方法和属性封装为独立模块,同时起到划分命名空间的作用,避免全局变量污染。

利用单例划分命名空间

4、单例模式用于划分命名空间

1)、防止全局声明的变量被意外修改。


/*using a namespace*/

var BHX = {};
BHX.Singleton = {
  attr1: true , 
  attr2: 10 ,
  method1 : function(){
    alert('我是方法1');
  },
  method2 : function(){
    alert('我是方法2');
  }        
};
BHX.Singleton.attr1;
var attr1 = false;

通过这种方式,即使外部声明了同名变量,也能在一定程度上防止内部属性(如attr1)被外部覆盖或修改,从而保护模块内部状态。

2)、防止其他来源代码的干扰与修改。

现代网页通常集成多种JavaScript代码来源,包括第三方库、广告脚本和徽章代码等。为避免代码冲突,最佳实践是定义一个包含自身所有逻辑的单例对象,将自有代码封装其中,从而隔离外部干扰。


var XGP = {};
XGP.Common = {
  //A singleton with common methods used by all objects and modules
}
XGP.ErrorCodes = {
  //An object literal used to store data
}
XGP.PageHandler = {
  //A singleton with page specific methods and attributes.
}

3)、用作专用代码的封装。

在大型网站中,部分代码为所有页面共用,通常存放于独立文件;而另一些代码仅服务于特定页面。将这两类代码分别封装在各自的单例对象中,有助于提高代码的组织性和复用性。

在Web开发中,常需为表单添加JavaScript交互功能。出于渐进增强和平稳退化的考虑,通常先构建一个不依赖JavaScript、使用标准HTML提交机制的纯HTML页面,再在此基础上叠加JS增强功能。


XGP.RegPage = {
  FORM_ID: 'reg-form',
  OUTPUT_ID: 'reg-result',

  handleSubmit: function(e){
    e.preventDefault(); //stop the normal form submission

    var data = {};
    var inputs = XGP.RegPage.formEl.getElementByTagName('input');

    for(var i=0, len=inputs.length; i<len; i++){
      data[inputs[i].name] = inputs[i].value;
    }

    XGP.RegPage.sendRegistration(data);
  },
  sendRegistration: function(data){
    //make an xhr request and call displayResult() when response is recieved
    ...
  },
  displayResult: function(response){
    XGP.RegPage.outputEl.innerHTML = response;
  },
  init: function(){
    XGP.RegPage.formEl =$(XGP.RegPage.Form_ID);
    XGP.RegPage.outputEl = $(XGP.RegPage.OUTPUT_ID);
    //hijack the form submission
    addEvent(XGP.RegPage.formEl, 'submit', XGP.RegPage.handleSubmit);
  }
}
//invoke initialization method after the page load
addLoadEvent(XGP.RegPage.init);

惰性加载与分支技术实现

5、惰性单例

前文所述的单例模式均存在一个共同点:单例对象在脚本加载阶段即被创建。对于资源消耗大或配置复杂的单例,更合理的做法是将其实例化过程推迟到真正需要使用该实例的时刻。

这种技术被称为惰性加载(lazy loading)。

实现步骤如下:

1).将所有初始化代码移至构造函数(constructor)中。

2).全权控制实例化的调用时机,这正是getInstance方法所承担的职责。


XGP.lazyLoading = (function(){
  var uniqInstance;

  function constructor(){
    var attr = false;
    function method(){

    }

    return {
      attrp: true,
      methodp: function(){

      }
    }
  }

  return {
    getInstance: function(){
      if(!uniqInstance){
        uniqInstance = constructor();
      }
      return uniqInstance;
    }
  }
})();

6、分支技术

分支技术是一种在运行时动态设置浏览器差异封装的方法,旨在解决跨浏览器兼容性问题。


// 分支单例 (判断程序的分支 <浏览器差异的检测>)
var Ext = {} ;
var def = false ;
Ext.More = (function(){
  var objA = {    // 火狐浏览器 内部的一些配置
      attr1:'FF属性1'
      // 属性1 
      // 属性2 
      // 方法1 
      // 方法2
  } ;
  var objB = {    // IE浏览器 内部的一些配置
      attr1:'IE属性1'
      // 属性1 
      // 属性2 
      // 方法1 
      // 方法2             
  } ;
  return (def) ?objA:objB;
})();
alert(Ext.More.attr1);

例如,若网站频繁使用XMLHttpRequest(XHR),每次调用都执行浏览器嗅探代码将严重降低效率。更高效的策略是在脚本加载时一次性确定针对当前浏览器的代码分支。这正是分支技术的核心价值。当然,分支技术并非总是最优解,若多个分支中仅有一个被实际使用,其余分支仍会占用内存空间,造成资源浪费。

在决定是否使用分支技术时,必须在执行时间缩短与内存占用增加之间进行权衡。

下面利用分支技术实现XHR的封装示例:


var XHR = (function(){
  var standard = {
    createXhrObj: function(){
      return new XMLHttpRequest();
    }
  };
  var activeXNew = {
    createXhrObj: function(){
      return new ActiveXObject('Msxml2.XMLHTTP');
    }
  };
  var activeXOld = {
    createXhrObj: function(){
      return new ActiveXObject('Microsoft.XMLHTTP');
    }
  };

  var testObj;
  try{
    testObj = standard.createXhrObj();
    return testObj;
  }catch(e){
    try{
      testObj = activeXNew.createXhrObj();
      return testObj;
    }catch(e){
      try{
        testObj = activeXOld.createXhrObj();
        return testObj;
      }catch(e){
        throw new Error('No XHR object found in this environment.');
      }
    }
  }
})();

单例模式的局限性与总结

7、单例模式的弊端

在深入理解单例模式的优势后,我们还需审视其潜在缺陷。

由于单例模式提供的是单点访问入口,这可能导致模块之间产生强耦合。这种紧密耦合关系使得单元测试变得困难,因为测试单个模块时往往需要处理其依赖的单例状态。

综上所述,单例模式更适合用于定义命名空间和实现分支型方法等场景,而非所有需要单一实例的地方。

通过上述七个方面的详细解析,相信读者对单例模式有了更深入的理解。希望本文内容能为JavaScript开发者提供切实帮助。

作者最新文章