JavaScript设计模式之单例模式

单例模式又被称为单体模式,是只允许实例化一次的对象类。实现的方法一般是先判断实例中是否存在,如果存在则直接返回,不存在就创建了再返回,这样就确保了一个类只有一个实例对象。在JavaScript中,单例模式作为一个命名空间提供者,从全局命名空间里提供一个唯一的访问点来访问改对象。

  1. 保证一个类仅有一个实例,并提供一个访问它的全局访问点
    • '意图:' 保证一个类仅有一个实例,并提供一个访问它的全局访问点。
    • '主要解决:'一个全局使用的类频繁地创建与销毁。
    • '何时使用:'当您想控制实例数目,节省系统资源的时候。
    • '如何解决:'判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
  2. 因此要实现一个单例模式无非是使用一个变量来标记当前是否已经为某个类创建 了对象,如果创建则在下一次直接获取之前返回创建的实例

单例的使用场景:

  • 模块间通信
  • 系统中某个类的对象只能存在一个
  • 保护自己的属性和方法

单例模式的实现

在实现单例模式前,首先考虑如何保证一个类仅有一个实例?

一般情况下,当我们创建了一个类(本质是构造函数)后,可以通过new关键字调用构造函数进而生成任意多的实例对象。像这样:

class SingleDog {
        show() {
          console.log("我是一个单例对象");
        }
      }
      const s1 = new SingleDog();
      const s2 = new SingleDog();
      console.log(s1 === s2); //false

在上面的代码中,s1和s2两者是相互独立的对象,各占一块内存对象,而单例模式想要做到的是,不管我们尝试去创建多少次,它都只给你返回第一次所创建的那唯一的一个实例。要做到这一点,就需要构造函数具备判断自己是否已经创建过一个实例的能力。

class SingleDog {
        show() {
          console.log("我是一个单例对象");
        }
        static getInstance() {
          // 判断是否已经new过1个实例
          if (!SingleDog.instance) {
            // 若这个唯一的实例不存在,那么先创建它
            SingleDog.instance = new SingleDog();
          }
          // 如果这个唯一的实例已经存在,则直接返回
          return SingleDog.instance;
        }
      }

      const s1 = SingleDog.getInstance();
      const s2 = SingleDog.getInstance();
      console.log(s1 === s2); //true

 es5实现单例模式

var Singleton = function (name) {
    this.name = name
}
// 静态属性(类属性)
Singleton.instance = null

Singleton.prototype.getName = function () {
    console.log(this.name)
}
// 静态
Singleton.getInstance = function (name) {
    if(!Singleton.instance){
        Singleton.instance = new Singleton(name)
    }
    return Singleton.instance
}
var a = Singleton.getInstance('wang')
var b = Singleton.getInstance('Yi')

console.log(a.name) // wang
console.log(b.name) // wang
console.log(a === b ) // true

这种方式获取对象 虽然简单,但是这种实现方式不透明。知道的人可以通过 Singleton.getInstance() 获取对象, 不知道的需要研究代码的实现,这样不好。这与我们常见的用 new 关键字来获取对象有出入, 实际意义不大。

用getInstance 方法用闭包的方式

var Singleton = function (name) {
    this.name = name
}

Singleton.prototype.getName = function () {
    console.log(this.name)
}
// 静态
Singleton.getInstance =( function () {
    var instance = null
    return function (name) {
        if(!instance){
            instance = new Singleton(name)
        }
        return instance
    }
})()
var a = Singleton.getInstance('wang')
var b = Singleton.getInstance('Yi')

console.log(a.name) // wang
console.log(b.name) // wang
console.log(a === b ) // true

透明的单例模式

var CreateDiv = (function (html) {
    var instance
    // 实际创建的构造函数,也就是最后实际
    // 生成的实例的构造函数
    var CreateDiv = function (html) {
        if(instance){
            return instance
        }
        this.html = html
        this.init()
        return instance = this // 返回第一次创建的实例并且通过instance记录
    };

    // 初始化创建方法,会创建div标签
    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')
b.init()

透明的单例模式是为了可以向正常创建实例一样通过'new'的形式使用,整体思路是用一个值用来记录创建 的实例,如果已经创建则用记录的实例,如果没有创建则创建一个新的实例。

使用代理模式创建

1.代理模式:自己不去做,委托中间人做

2.Person是一个普通类,通过new Person可以创建一个对象

3.用代理模式创建CreateSinglePerson方法,通过new CreateSinglePerson可以创建一个单例

function Person(name) {
    this.name = name;
}
Person.prototype.getName = function () {
    console.log(this.name);
};
var CreateSinglePerson = (function () {
    var instance;
    return function (name) {
        if (!instance) {
            instance = new Person(name);
        }
        return instance;
    };
})();
var a = new CreateSinglePerson('a');
var b = new CreateSinglePerson('b');
console.log(a === b); // true
var c = new Person('c');
var d = new Person('d');
console.log(c === d); // false

惰性单例

惰性单例是指在需要的时候才创建,和之前的几个案例写法上是一样的,当使用的时候去new 或者去调用静态方法创建实例

var Singleton = function (name) {
    this.name = name
}

// 静态属性(类属性)
Singleton.instance = null

Singleton.prototype.getName = function () {
    console.log(this.name)
}
// 静态
Singleton.getInstance = function (name) {
    if(!this.instance){
        this.instance = new Singleton(name)
    }
    return this.instance
}
var a = Singleton.getInstance('wang')
var b = Singleton.getInstance('Yi')

console.log(a.name) // wang
console.log(b.name) // wang
console.log(a === b ) // true

通用惰性单例

var singleton = function(fn) {
    var instance;
    return function() {
        return instance || (instance = fn.apply(this, arguments));
    }
};

es6写单例

class Singleton{
    static instance = null

    constructor(name ){
        this.name = name
    }

    getName(){
        console.log(this.name)
    }

    static getInstance(name){
        if(!Singleton.instance){
            Singleton.instance = new Singleton(name)
        }
        return Singleton.instance
    }
}
const a = Singleton.getInstance('wang')
const b = Singleton.getInstance('yi')
a.getName()
b.getName()

es6透明单例

class Singleton{
    static instance = null

    constructor (name ){
        if(Singleton.instance){
            return Singleton.instance
        }
        this.name = name
        return Singleton.instance = this
    }

    getName(){
        console.log(this.name)
    }

}
const a = new Singleton('wang')
const b = new Singleton('yi')
a.getName()
b.getName()
console.log(a === b)

es6代理模式写单例

class Person{
    constructor (name ){
        this.name = name
    }
    getName(){
        console.log(this.name)
    }
}
// 我是代理
var CreateSinglePerson = (function () {
    var instance;
    return function (name) {
        if (!instance) {
            instance = new Person(name);
        }
        return instance;
    };
})()
var a = new CreateSinglePerson('a');
var b = new CreateSinglePerson('b');
console.log(a === b); // true

应用

系统中某个类的对象只能存在一个

  例如,我们要实现点击按钮,弹出一个模态框

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <style>
        div{
            width:200px;
            height:200px;
            border:1px solid #09f;
            position: absolute;
        }
    </style>
</head>
<body>
    <input type="button" value="弹窗">
    <script>
        var oBtn = document.querySelector("input"),
        offset = 20, index = 1;
        function Module(pos){
            this.offset = pos || 20;
        };
        Module.prototype.create = function(){
            var oDiv = document.createElement("div");
            oDiv.style.left = (++index) * offset + 'px';
            oDiv.style.top = (++index) * offset + 'px';
            oDiv.innerHTML = '普通弹窗';
            return oDiv;
        };
        oBtn.onclick = function(){
            var oDiv = new Module();
            document.body.appendChild(oDiv.create());
        };
    </script>
</body>
</html>

我们希望的是,不管点击按钮多少次,都只出现一个模态框,但结果却是下面这样的:

 

 这个时候就需要用单例模式进行改造:

 <!DOCTYPE html>
 <html lang="en">
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <meta http-equiv="X-UA-Compatible" content="ie=edge">
     <title>Document</title>
     <style>
         div{
             width:200px;
             height:200px;
             border:1px solid #09f;
             position: absolute;
         }
     </style>
 </head>
 <body>
     <input type="button" value="弹窗1">
     <input type="button" value="弹窗2">
     <script>
         var oBtn = document.querySelectorAll("input"),
         offset = 20, index = 1;
         function Module(pos){
             this.offset = pos || 20;
         };
         Module.prototype.create = function(){
             var oDiv = document.createElement("div");
             oDiv.style.left = (++index) * offset + 'px';
             oDiv.style.top = (++index) * offset + 'px';
             oDiv.innerHTML = '单例模式弹窗';
             return oDiv;
         };
         Module.one = (function(){
             var ins = null, isExist = false;
             return function(pos){
                 if(!ins) ins = new Module(pos);
                 if(!isExist){
                     document.body.appendChild(ins.create());
                     isExist = true;
                 }
             }
         })();
         oBtn[0].onclick = function(){
             Module.one(10);
         };
         oBtn[1].onclick = function(){
             Module.one(10);
         };
     </script>
 </body>
 </html>

在Module.one中通过变量isExist的两种状态和闭包特性控制元素只能被添加一次,就可以实现只能弹出一个模态框的效果了。

全局唯一的Modal弹框实现

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>单例模式弹框</title>
  </head>
  <style>
    #modal {
      height: 200px;
      width: 200px;
      line-height: 200px;
      position: fixed;
      left: 50%;
      top: 50%;
      transform: translate(-50%, -50%);
      border: 1px solid black;
      text-align: center;
    }
  </style>
  <body>
    <button id="open">打开弹框</button>
    <button id="close">关闭弹框</button>
  </body>
  <script>
    // 核心逻辑,这里采用了闭包思路来实现单例模式
    const Modal = (function () {
      let modal = null;
      return function () {
        if (!modal) {
          modal = document.createElement("div");
          modal.innerHTML = "我是一个全局唯一的Modal";
          modal.id = "modal";
          modal.style.display = "none";
          document.body.appendChild(modal);
        }
        return modal;
      };
    })();

    // 点击打开按钮展示模态框
    document.getElementById("open").addEventListener("click", function () {
      // 未点击则不创建modal实例,避免不必要的内存占用;此处不用 new Modal 的形式调用也可以,和 Storage 同理
      const modal = new Modal();
      modal.style.display = "block";
    });

    // 点击关闭按钮隐藏模态框
    document.getElementById("close").addEventListener("click", function () {
      const modal = new Modal();
      if (modal) {
        modal.style.display = "none";
      }
    });
  </script>
</html>

保护自己的属性和方法

单例模式经常为我们提供一个命名空间。例如我们使用过的jQuery,单例模式就为它提供了一个命名空间jQuery。

 在上面的代码中,因为可用的单词有限,命名十分简单,但是如果后续后其他的同事在维护代码的时候,出现了同名的方法或变量,这里的业务逻辑就会出现问题,此时就需要用命名空间来约束每一个人定义的变量:

  

 由于对象中的this指代当前对象,所以,上面两种写法是等效的。

生产实践:Vuex中的单例模式

 Vuex 使用单一状态树,用一个对象就包含了全部的应用层级状态。至此它便作为一个“唯一数据源 (SSOT)”而存在。这也意味着,每个应用将仅仅包含一个 store 实例。单一状态树让我们能够直接地定位任一特定的状态片段,在调试的过程中也能轻易地取得整个当前应用状态的快照。 ——Vuex官方文档

Store 是一个“假单例”

在这里,假单例的意思是虽然没有严格遵循单例模式的设计原则,但在实际应用中仍然能够保证实例的唯一性。

Vuex 中的 Store 就是这样一个”假单例“—— 尽管在实际应用中通常 Store 只有一个全局实例,但从实现上来看,它并不是一个严格意义上的单例模式。

 

在 Vuex 中,我们可以通过 new Vuex.Store(options)  调用构造函数来创建一个新的 Store 实例。而在楼上贴出的 Store 的 constructor  关键源码中,并不存在任何和单例有关的识别/拦截逻辑。这意味着开发者可以通过 new 关键字创建多个 Store 实例,这显然不符合我们对单例模式的预期。

Vuex 如何确保 Store 的单例特征

Store 并没有实现标准的单例模式,但是却能够表现出一种类似于单例的行为。这是因为 Vuex 从整体设计的层面来保证了 Store 在同一个 Vue 应用中的唯一性。具体来说,我们首先需要关注的是 Vue.use() 方法,这个方法允许我们给 Vue 应用安装像 Vuex 这样的插件。Vuex 插件是一个对象,它在内部实现了一个 install 方法,这个方法会在插件安装时被调用,从而把 Store 注入到 Vue 应用里去。也就是说每 install 一次,Vuex 都会尝试给 Vue 应用注入一个 Store

在 install 函数源码中,有一段和我们楼上的 getInstance() 非常相似的逻辑,通过判断当前 Vue 应用是否已经安装过 Vuex 插件,保证了在同一个 Vue 应用中只存在一个 Vuex 实例

 在 install 函数中,我们可以看到 Vue 实例被赋值为 _Vue,接着作为 applyMixin(Vue) 函数的参数触发一次 applyMixin() 的调用。applyMixin() 函数会在 Vue 实例的 beforeCreate 生命周期钩子中,将 Store 实例挂载到 Vue 实例上。这个“挂载”动作对应的是如下所示的 vuexInit() 函数。

这段代码中最值得我们注意的,是 else if 这一行的判断:如果当前组件实例的配置对象中不存在 store,但存在父组件实例(options.parent)且父组件实例具有 $store 属性,那么将父组件实例的 $store 赋值给当前组件实例的 $store
这段逻辑意味着,$store实例在 Vue 组件树中是被层层继承下来的——当子组件自身不具备 $store 时,会查找父组件的 $store 并继承。这样,整个 Vue 组件树中的所有组件都会访问到同一个 Store 实例——那就是根组件的Store实例。也就是说,vuexInit()的主要作用是将根组件的Store实例注入到子组件中,这样所有子组件都可以通过this.$store访问到同一个 Store 实例。这就确保了 Vuex Store 在整个 Vue 应用中的唯一性。

“单例 Store”的局限性

我们在上面所探讨的“Store 的唯一性“是有前提的——这种唯一性是针对同一个 Vue 应用来说的,而不是针对全局来说的。

在全局范围内,Vuex 中的 Store 并不一定是唯一的。因为在同一个页面中,我们可以使用多个 Vue 应用,每个 Vue 应用都可以拥有自己的 Store 实例。这也解释了为什么Vuex没有将单例逻辑放在Store 类中去实现,而是将其解构到了 install 函数里。

在同一个 Vue 应用中,只会存在一个 Store 实例,但在多个 Vue 应用中,可以存在多个 Store 实例。在不同的 Vue 应用中,当我们想共享唯一的一个 Store 时,仍然需要通过在全局范围内使用单例模式来确保 Store 的唯一性。

总结

Vuex 的设计遵循了单例模式的思想,通过 install() 函数拦截 Vue.use(Vuex)的多次调用,确保了在同一个 Vue 应用中只会安装唯一一个 Vuex 实例;通过 vuexInit() 函数,确保了同一个 Vue 应用只会挂载唯一一个 Store。这样一来,从效果上来看,Vuex 确实是创造了两个”单例“出来。

这种设计使得在整个 Vue 应用中,所有组件都能方便地访问同一个 Store 实例。这有助于在整个应用范围内维护一致的状态管理,降低了程序的复杂性。

尽管 Vuex 并不是严格意义上的单例模式,但它却很大程度上从单例模式的思想中受益,也为我们在实践中应用单例模式提供了全新的思路。

posted on 2020-01-01 13:43  紅葉  阅读(461)  评论(0编辑  收藏  举报