乐趣区

关于javascript:用JS来理解设计模式五-单例模式

单例模式

单例模式 确保一个类只有一个实例,并提供一个全局拜访点。

单例模式应该算是一个设计模式中比较简单且好用的模式,大部分人或多或少都接触过这个模式。所以也不举什么例子了,先来问几个问题,如果都理解的话,也就不必往下看了。

  • 单例模式有什么用,什么时候用?
  • 单例模式怎么写?
  • 在 JS 里用单例模式时可能会出什么问题?

单例模式有什么用,什么时候用?

很简略,在下面的定义以及模式的名字就能看进去,这个模式的用途就是保障咱们拿到的对象永远是同一个,不论是在代码的什么中央,也不论是在何时。

举几个例子,比方说一个提醒弹窗,一般来说这种提醒弹窗通常不会同时展现多个进去,但如果短时间内屡次调用生成提醒弹窗的办法,则有可能会呈现多个提醒弹窗。但假若采纳了单例模式来,咱们拿到的永远是同一个提醒弹窗,只会在同一个提醒弹窗上进行批改,就不必放心多个弹窗呈现的状况。

再比方说,当咱们创立的一个对象所花销的工夫较长,且这个对象属于可复用的,又可能须要频繁应用时,就能够采纳单例模式,比拟常见的就是创立编辑器这类对象等等。

书里有提到一个大家可能会有的问题,那就是写一个大家约定好的全局变量不也一样吗?因为这样也可能实现单例模式所须要的成果,无论是在何时何地拿到的都是同一个对象。的确这样子也是能够做到单例的成果,然而有几个问题。

1. 如果创立这个对象过程十分耗费资源,而后咱们却始终没用应用到,那么不就是造成节约了吗,即节约了生成的工夫,也节约了其占据的内存空间。

2. 如何保障这个对象只会被实例化一次?生成这个对象的办法会不会裸露进去,使得其余开发人员抉择在其余中央本人调用?

这里也说到了一个很重要的点,就是结构这个对象的办法该当是“公有”的,不可能被间接调用,否则这个办法也没什么意义了。

单例模式怎么写?

首先须要和大家阐明,我写这个代码是依据本人的了解来写的,写之前没参考他人的写法,所以可能会有一些问题,欢送大家指出。

let getTestInstance = (function () {
  let instance = null;
  class Test {constructor (name) {this.name = name || 'test'}
    changeName (name) {this.name = name;}
  }
  return function (...args) {if (!instance) instance = new Test(...args);
    return instance;
  }
})();

let a = getTestInstance();
console.log(a.name); // test
a.changeName('name change');
console.log(a.name); // name change

let b = getTestInstance();
console.log(b.name); // name change

b.changeName('name change2');
console.log(b.name); // name change2
console.log(a.name); // name change2
console.log(a === b); // true

这里的实现比较简单,次要是通过闭包,将咱们的单例和构造函数“私有化”,无奈间接拜访,从而保障拿到的都是同一个对象,且只能通过 getTestInstance 办法来获取。这样咱们就能确保一个类只有一个实例,且只能通过我提供的这个拜访点来获取,这样就合乎了单例模式的要求。

单例模式 确保一个类只有一个实例,并提供一个全局拜访点。

咱们能够看到变量 a 与变量 b 其实指向的都是同一个实例,因而不论是 a 对实例的扭转或是 b,都会相互影响。

这里的代码实现例子是一个简略的例子,不过还有一些非凡的状况须要思考。

在 JS 里用单例模式时可能会出什么问题?

  1. 多线程问题
  2. 异步生成实例问题

多线程问题

多线程问题指的是假若有几处代码同时调用了获取实例办法,且实例尚且未生成,那么可能呈现这几处的实例不统一的状况。家喻户晓 JS 是一个单线程模型,所有工作只能在一个线程上实现,一次只能做一件事。后面的工作没做完,前面的工作只能等着。

不过当初有了 webWorker,JS 也领有了多线程环境,那么咱们就能够进行尝试在 JS 多线程中是否会存在单例模式的多线程问题。

思路是:新建多个 webworker,并且在 webWorker 中引入 getTestInstance 办法,而后将 webWorker 中的生成的实例返回给主流程,在主流程中进行对象比拟。

然而通过尝试发现,在 webWorker 中应用 importScripts 引入的 js 是相互独立的,这意味着生成的实例都是不一样的,那么引入 js 这条路子就断了。

因而我就想方法将主流程中的 getTestInstance 办法传入到 webWorker 外面,其中我采纳了两种办法,但都以失败告终。别离是:

  1. getTestInstance 办法用 postMessage 传入。
  2. 因为 webWorker 中线程所在的全局对象,与主线程不一样,无奈读取主线程所在网页的 DOM 对象,也无奈应用 documentwindowparent 这些对象。然而,Worker 线程领有 navigator 对象和 location 对象。利用这个特点,我尝试将 getTestInstance 办法挂在 location 对象上。

但两者的后果别离是:

  1. 无奈将办法传入到 webWoker,这里应该是底层限度,无奈解决。
app.js:15 Uncaught DOMException: Failed to execute 'postMessage' on 'Worker': function (...args) {if (!instance) instance = new Test(...args);
      return instance;
    } could not be cloned. 
  1. 尽管 webWorker 的确领有 location 对象,但它的 location 对象很非凡,是 WorkerLocation,且不同 Worker 之间的WorkerLocation 都是独立的。因而无奈在 location 对象上获取到 getTestInstance 办法。

因而能够全面的说在 JS 多线程中无奈应用单例模式。

异步生成实例问题

这个问题次要是探讨如果生成实例的办法是一个异步的办法,那么在异步办法执行实现前,有别处代码调用了获取实例办法,那么是不是会产生屡次调用或实例不统一的问题?

因而实现上有所不同,但其实也很简略,就是返回的值变成 Promise 且多加几个状态管制而已,上面是实现代码,如有脱漏或谬误,欢送指出。

let getTestInstance = (function () {
    let instance = null;
    let loading = false; // 判断是否正在生成中
    let loadingPrmoise = null; // 用于记录第一次生成的 promise
    class Test {constructor (name) {this.name = name || 'test'}
      changeName (name) {this.name = name;}
    }
    return function (...args) {if (instance) return Promise.resolve(instance);
      if (loading) return loadingPrmoise;
      loading = true;
      // 一个 2S 的生成实例过程
      let promise = new Promise((resolve, reject) => {setTimeout(() => {instance = new Test(...args);
          loading = false;
          resolve(instance);
        }, 2000)
      });
      loadingPrmoise = promise;
      return promise;
    }
  })();

  let a,b;
  // 生成实例须要 2S
  getTestInstance().then(res => {a = res;});
  // 1S 秒再次获取实例
  setTimeout(() => {getTestInstance().then(res => {
      b = res;
      console.log(a === b) // true
      getTestInbstance().then(res => {console.log(a === res); // true
      })
    });
  }, 1000);
退出移动版