深入理解JavaScript设计模式之单例模式

设计模式不是"炫技",而是"沉淀"

什么是单例模式

单例模式(Singleton Pattern)就像'独生子女',确保某个类在整个系统中只有一个实例,并提供一个全局访问点。简单来说,一个类一辈子只有一个孩子。在 JavaScript 编程世界中,例如 Web 应用中的登录窗口,无论你点击多少次登录按钮,只会弹出一次。

为什么需要单例模式

想象一下,你是DR钻戒的老板。我们推出一款特殊的钻戒------"单例钻戒"。这款钻戒一生只能购买一次,每位顾客终身仅能拥有一枚。这正是单例模式(Singleton Pattern)的生动写照。

常见应用场景包括

  1. 浏览器中的任务管理器或标签页管理:保证只有一个全局管理器在运行。
  2. 通知中心 / 消息中心:统一处理应用内的消息推送和响应。
  3. 侧边栏 / 弹窗组件:页面中始终只显示一个实例,避免重复创建。
  4. 全局配置文件管理:读取并维护一份全局配置,供整个应用使用。
  5. VueVuexReactContextRedux)和组件生命周期管理机制。

通过单例模式,我们可以有效地控制资源的使用,减少重复对象的创建,提升系统性能与稳定性。

单例模式实现

透明单例模式实现

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( 'sven1' ); 
	var b = Singleton.getInstance( 'sven2' ); 
	alert ( a === b ); // true

上面的代码中是一个简单实现单例模式的方式,通过调用Singleton.getInstance()来获取Singketon类的唯一实例,实现很简单,但是有一个缺点,降低了类的"透明性"。 通常情况下,使用一个类的时候,习惯用new ClassName()的方式来新建一个对象,但是Singleton类并不是通过new直接创建实例的,而是通过Singleton.getInstance()这个静态方法来获取唯一的实力,这就必须要求使用这个类的人必须知道它是一个单例模式,并且要记住不能用new来创建对象,必须使用Singleton.getInstance()来创建对象,这种写法会让类的使用变得不够直观和不透明,增加了使用上的认知负担,很容易导致吴用。

不透明单例模式

javascript 复制代码
 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("sven1");
      var b = new CreateDiv("sven2");
      console.log("a:", a);
      console.log("b:", b);
      console.log(a === b); 

在上一步骤中,我们实现了一个不透明的单例模式,即用户必须通过 Singleton.getInstance() 的方式来获取实例,而不是使用常见的 new 方式创建对象。这种方式虽然实现了单例的效果,但不够直观,降低了类的"透明性"

为了改进这一点,我们可以尝试实现一个透明的单例模式。所谓"透明",是指用户在使用时可以像使用普通类一样,直接通过 new CreateDiv() 的方式来创建实例,而无需关心这是一个单例类。 我们通过一个自执行的闭包函数来实现这个目标。在这个闭包中,我们将实例变量(如 instance)封装为"私有变量",使其不会被外部轻易访问或修改,并且在整个页面不刷新的情况下,始终只存在一个实例。

例如,我们定义了一个 CreateDiv 类,它在被 new 调用时会检查是否已经存在实例,如果存在,则返回已有的实例,从而保证了全局唯一性。整个过程对使用者是透明的,使用方式和普通类完全一致,这样一来,既保留了单例的核心特性,又提升了使用的直观性和代码的可维护性。

虽然很流畅,很有单例的感觉,但是透明的单例模式还是有缺点的:

  1. 代码复杂度提高了:为了封装单例逻辑,使用了闭包和自执行函数,让代码变得不容易理解。
  2. 构造函数职责不单一:createDiv构造函数不仅要负责创建对象和初始化,还要控制只能有一个单例的逻辑,违反了"单一职责原则"
  3. 不容易扩散或者修改:如果我们还想把这个类从"只能创建一个实例"改为"可以创建多个实例",就必须去修改构造函数内部的逻辑,非常不灵活,虽然这个写法让使用更自然(像普通类一样用 new 创建实例),但实现起来更复杂,维护成本也更高

用代理实现单例模式

javascript 复制代码
var CreateDiv = function (html) {
    this.html = html;
    this.init();
  };
  CreateDiv.prototype.init = function () {
    var div = document.createElement("div");
    div.innerHTML = this.html;
    document.body.appendChild(div);
  };
  var ProxySingletonCreateDiv = (function () {
    var instance;
    return function (html) {
      if (!instance) {
        instance = new CreateDiv(html);
      }
      return instance;
    };
  })();
  var a = new ProxySingletonCreateDiv("sven1");
  var b = new ProxySingletonCreateDiv("sven2");
  console.log("a", a);
  console.log("b", b);
  console.log(a === b);

上面代码对透明单例模式的代理模式进行了优化,原来的CreateDiv类在构造函数中直接包含了控制单例的逻辑导致职责不清楚,不容易扩展,现在通过引入一个代理类ProxySingletonCreateDiv,将管理单例的逻辑从CreateDiv中抽离出去,让它回归为一个存粹用于创建div元素的普通类,具体来说,CreateDiv只负责创建对象和初始化操作,比如生成div并插入页面,单例的逻辑则有代理函数ProxySingletonCreateDiv来处理,这个代理函数是一个闭包,内部维护了一个唯一的实例,当第一次调用的时候会创建CreativeDiv实力,之后直接返回该实例,这种方法是缓存代理的一种典型应用,让CreateDiv更加的灵活,通用,同时又不丢失单例的特性。

javaScript中的单例模式

上面的集中单例模式实现方式偏向于传统的面向对象的写法,即通过"类"来创建唯一的对象,但是在javaScript中,这种做法也不完全适用,javaScriptes5中是一门无类(class-free)语言,创建对象非常简单,如果只需要一个唯一的对象,没必要先定义一个类再来创建实例,要知道单例模式的核心即可:"确保只有一个实例,并提供全局访问",最然全局变量具备唯一全局访问的特性,但是它并不是真正的单例模式,而且存在命名冲突,全局作用域污染,在大型的项目中使用全局作用域尤其危险,为了减少全局变量污染带来的问题,可以采取使用命名空间和使用闭包封装私有变量解决。

使用命名空间

适当的使用命名空间,并不会杜绝全局变量,但是可以减少全局变量的数量,最简单的方法就是对象的字面量的方式解决:

javascript 复制代码
var namespace1 = {
     a: function () {
       alert(1);
     },
     b: function () {
       alert(2);
     },
   };

ab都定义为 namespace1 的属性,这样可以减少变量和全局作用域打交道的机会。

使用闭包封装私有变量

javascript 复制代码
  var user = (function () {
        var __name = "sven",
          __age = 29;
        return {
          getUserInfo: function () {
            return __name + "-" + __age;
          },
        };
      })();

将一些变量封装在闭包的内部,只暴露一些接口跟外界通信,用下划线来约定私用变量__name__age,它们被封装在闭包产生的作用域中,外部变量访问不到这两个变量,这就进行了全局的隔绝污染。

惰性单例

前面了解了单例模式的一些实现方法,接下来学习一下单例模式比较重要的一个知识"惰性单例【Lazy Singleton】",惰性单例指的是需要的时候才创建实例,而不是在页面加载的时候就提前创建,这种方式更节省资源,实际开发中,以弹窗为例,如果一开始就创建弹窗元素,用户可能根本不需要弹出窗口,这样回造成资源浪费,如果每次点击按钮都创建一个新的弹窗,虽然实现了按需展示,但却失去了单例的唯一性,正确的做法是首次点击创建弹窗,再次点击直接使用已经创建好的弹窗实例,这样就实现了惰性单例,即实现了只创建一次,又坐到了用的时候才创建,兼顾了功能与性能。

javascript 复制代码
<html>
  <head>
    <style>
      .modal {
        position: fixed;
        width: 200px;
        height: 200px;
        line-height: 200px;
        text-align: center;
        left: 50%;
        top: 50%;
        transform: translate(-50%, -50%);
        background: goldenrod;
      }
    </style>
  </head>
  <body>
    <button id="open">打开</button>
    <button id="close">关闭</button>
    <script>
      const Moda = (function () {
        let instance = null;
        return function () {
          if (!instance) {
            instance = document.createElement("div");
            instance.innerHTML = "模态框";
            instance.className = "modal";
            instance.style.display = "none";
            document.body.appendChild(instance);
          }
          return instance;
        };
      })();
      document.getElementById("open").onclick = function () {
        // 创建一个模态框
        const modal = Moda();
        modal.style.display = "block";
      };
      document.getElementById("close").onclick = function () {
        // 删除模态框
        const modal = Moda();
        modal.style.display = "none";
      };
    </script>
  </body>
</html>

通用的惰性单例

我们知道了什么是惰性单例和如何实现惰性单例,但是不难发现,普通的惰性单例还是违反了单一职责原则,创建对象和管理单例的逻辑都放在了Moda内部,如果我们下次需要创建页面唯一的iframe或者script或者更多类型的标签,就必须如法炮制创建更多的Moda函数,几乎照抄一遍,针对这种方法需要将不变的部分隔离出来,创建diviframeimg逻辑其实可以完全抽象出来【使用一个变量标志是否创建过,如果创建过则下次直接返回这个已经常见好的对象】。

javascript 复制代码
return function () {
  if (!instance) {
     instance = document.createElement("div");
     ...    
   }
   return instance;
 };

接下来就把如何管理单例的逻辑抽离出来,这些逻辑封装在getSingle函数内部,创建对象的方法fn被当成参数动态传入getSingle函数。

javascript 复制代码
var getSingle = function( fn ){ 
 var result; 
 return function(){ 
 return result || ( result = fn .apply(this, arguments ) ); 
 } 
}; 

接下来将用于创建登录浮窗的方法用参数 fn 的形式传入 getSingle,我们不仅可以传入Mode,还能传入 ModeScriptModeIframeModeXhr 等。之后再让getSingle返回一个新的函数,并且用一个变量 result 来保存fn 的计算结果。result 变量因为身在闭包中,它永远不会被销毁。在将来的请求,如果 result 已经被赋值,那么它将返回这个值。

javascript 复制代码
<html>
  <head>
    <style>
      .modal {
        position: fixed;
        width: 200px;
        height: 200px;
        line-height: 200px;
        text-align: center;
        left: 50%;
        top: 50%;
        transform: translate(-50%, -50%);
        background: goldenrod;
      }
    </style>
  </head>
  <body>
    <button id="open">打开</button>
    <button id="close">关闭</button>
    <script>
      let getSingle = function (fn) {
        let result;
        return function () {
          return result || (result = fn.apply(this, arguments));
        };
      };
      let ModelDiv = function () {
        let div = document.createElement("div");
        div.innerHTML = "模态框";
        div.className = "modal";
        div.style.display = "none";
        document.body.appendChild(div);
        return div;
      };

      let ModelIframe = function () {
        let iframe = document.createElement("iframe");
        iframe.style.display = "none";
        document.body.appendChild(iframe);
        return iframe;
      };
      var createSingleLoginLayer = getSingle(ModelDiv);
      var createSingleLoginLayer2 = getSingle(ModelIframe);
      document.getElementById("open").onclick = function () {
        // 创建一个模态框
        const modal = createSingleLoginLayer();
        const ModelIframe1 = createSingleLoginLayer2();
        modal.style.display = "block";
        ModelIframe1.style.display = "block";
      };
      document.getElementById("close").onclick = function () {
        // 删除模态框
        const modal = createSingleLoginLayer();
        const ModelIframe2 = createSingleLoginLayer2();
        modal.style.display = "none";
        ModelIframe2.style.display = "none";
      };
    </script>
  </body>
</html>

结语

设计模式不是"炫技",而是"沉淀",希望我们通过阅读和学习《JavaScript设计模式》和实践中,在显示业务需求开发中写出更具有可维护性,可扩展性的代码。

致敬------ 《JavaScript设计模式》· 曾探

相关推荐
爱生活的苏苏2 分钟前
vue生成二维码图片+文字说明
前端·vue.js
拉不动的猪4 分钟前
安卓和ios小程序开发中的兼容性问题举例
前端·javascript·面试
炫彩@之星10 分钟前
Chrome书签的导出与导入:步骤图
前端·chrome
贩卖纯净水.20 分钟前
浏览器兼容-polyfill-本地服务-优化
开发语言·前端·javascript
前端百草阁26 分钟前
从npm库 Vue 组件到独立SDK:打包与 CDN 引入的最佳实践
前端·vue.js·npm
夏日米米茶27 分钟前
Windows系统下npm报错node-gyp configure got “gyp ERR“解决方法
前端·windows·npm
且白1 小时前
vsCode使用本地低版本node启动配置文件
前端·vue.js·vscode·编辑器
程序研1 小时前
一、ES6-let声明变量【解刨分析最详细】
前端·javascript·es6
siwangqishiq21 小时前
Vulkan Tutorial 教程翻译(四) 绘制三角形 2.2 呈现
前端
李三岁_foucsli1 小时前
js中消息队列和事件循环到底是怎么个事,宏任务和微任务还存在吗?
前端·chrome