vlambda博客
学习文章列表

被问到设计模式不要怂:一文读懂单例模式

何为单例

顾名思义,就是一个类只有一个实例,并可以全局访问。比如说一个系统只有一个登录弹窗入口,这个登录弹窗就适合用单例模式设计。

如何实现

怎么在创建一个实例的时候知道,这个类在之前有没有实例?

1. flag

 
   
   
 
  1. function A(name){

  2. this.name=name

  3. }


  4. A.instance=null


  5. A.getInstance=function(name){

  6. if(!this.instance){

  7. this.instance=new A(name)

  8. }

  9. return this.instance

  10. }

2. 闭包

将上例 getInstance改写:

 
   
   
 
  1. A.getInstance=(function(name){

  2. var instance=null

  3. return function(name){

  4. if(!instance){

  5. instance=new A(nanme)

  6. }

  7. return instance

  8. }

  9. })()

前面两个虽能解决单例的实现,但我每次如果要想创建单例,就必须调用 getInstance函数才可以,直接 new毫无作用,这样写不是很龟毛吗?

3. 闭包+同名覆盖

将上例稍微改动一下,就可直接 new

 
   
   
 
  1. var A=(function(){ // 注意用var

  2. var instance=null

  3. var A=function(){

  4. if(instance) return instance;

  5. this.init()

  6. return instance=this

  7. }

  8. A.prototype.init=function(){

  9. // do something

  10. }

  11. })()

这种方法虽然很妙,但可复用性不强,因为 A是有两个职责的:一是进行初始化,二是单例的判断。

4. 使用代理类

顾名思义,代理类就是进行代理,把主要职责转发。这里我们把A写为普通类。

 
   
   
 
  1. var A=function(name){

  2. this.name=name

  3. this.init()

  4. }


  5. A.prototype.init=function(){

  6. // do something

  7. }


  8. // 创建代理类

  9. var P=(function(){

  10. var instance=null

  11. return function(name){

  12. if(!instance){

  13. instance=new A(name)

  14. }

  15. return instance

  16. }

  17. })()


  18. const p1=new P('aaa')

  19. const p2=new P('bbb')

  20. alert(p1===p2) // true

在JS中的应用

由于社区更新迭代速度非常快,尤其是前端工程化出现的各种工具、框架和思想,让我们在开发的时候,对于变量的控制方面的困扰也少了很多,但我们不能仅仅做一个框架的使用者而仅仅局限于眼前的操作而已,要知道框架对于某些功能的实现也是基于底层语言的,所以搞清这些东西是非常有必要的。

我们都知道,js是一门 class-free语言,他是没有“类”这个概念的,es6引入的 class也只是语法糖而已,我们按照上面使用“类”的思想构造单例,相当于脱裤放屁,本身我们要做单例,只是需要一个“唯一”的对象,并且可以被全局访问到而已,而在js中创建一个对象十分容易,关于对象是否可被全局访问,当然是使用全局变量,而全局变量作为js一个经常被人诟病的东西,要如何处理使其污染最小呢?

1.命名空间

使用命名空间可以极大地减少全局变量,而且它创建起来十分简单。

 
   
   
 
  1. const namespace={

  2. a:function(){},

  3. b:{

  4. c:()=>{}

  5. }

  6. }

但如果是作为一个 immutable这远远不够,因为即使是用 const声明的对象类型,也会被不小心修改,所以就不得不将对象冻结,但棘手的是,这个对象中的原始类型是修改且扩展不了了,但对于此对象中嵌套的对象类型的属性,仍然可以被修改,如下:

 
   
   
 
  1. const namespace={

  2. a:function(){},

  3. b:{

  4. c:()=>{}

  5. }

  6. }


  7. Object.freeze(namespace)

  8. namespace.d='ddddd' // 不能扩展

  9. namespace.b='bbbbbb' // 不能修改属性

  10. namespace.b.c='cccccc' // 但内嵌的对象可以修改属性和扩展

  11. console.log(namespace)

  12. /* 输出

  13. a: ƒ ()

  14. b: {c: "cccccc"}

  15. */

那这里就不得不去将属性b进行处理,如下:

 
   
   
 
  1. Object.freeze(namespace)

  2. Object.defineProperty(namespace.b,'c',{

  3. writable:false,

  4. configurable:false

  5. })

  6. namespace.d='ddddd' // 不能扩展

  7. namespace.b='bbbbbb' // 不能修改属性

  8. namespace.b.c='cccccc' // 内嵌的对象也不可被修改

  9. console.log(namespace)

  10. /* 输出

  11. a: ƒ ()

  12. b: {c: ƒ}

  13. */

但这里有个弊端就是,我必须知道他有对象类型的属性再进行封装,如果对象属性中还有对象属性就需要一层层去递归实现,让人头大。

2.使用闭包封装私有变量

把私有变量封装在闭包的内部,只暴露一些接口。

 
   
   
 
  1. var namespace=(function(){

  2. const _a=function(){}

  3. const _b={

  4. c:()=>{}

  5. }

  6. return {

  7. getNameSpace:function(){

  8. return ({

  9. a:_a,

  10. b:_b

  11. })

  12. }

  13. }

  14. })()


  15. Object.freeze(namespace)

  16. namespace.getNameSpace='1111'

  17. console.log(namespace.getNameSpace())

  18. /*输出:

  19. a: ƒ ()

  20. b: {c: ƒ}

  21. */

惰性单例

惰性单例是当只有需要的时候才会创建对象实例,就比如在第一节中我们实现的那个例子:

 
   
   
 
  1. function A(name){

  2. this.name=name

  3. }


  4. A.instance=null


  5. A.getInstance=function(name){

  6. if(!this.instance){

  7. this.instance=new A(name)

  8. }

  9. return this.instance

  10. }

但这是基于“类”的设计,上一节讲,在js中写这种单例模式等于脱裤放屁,那什么样的单例模式的实现才最有普适性呢?我将引入一个例子来分析。

在一个系统中,我们需要一个登录按钮,点击登录按钮需要弹出登录弹窗。

有两种设计思想可以实现,一是把弹窗组件写好先隐藏起来,之后通过改变css让其显示;二是当用户点击登录按钮时再去创建弹窗。因为本节讨论的是惰性单例,所以前者的实现不再讨论范围之内,所以我们来实现一下后者。

 
   
   
 
  1. const createModal=function(){

  2. let modal=null

  3. return function(){

  4. if(!modal){

  5. modal=document.createElement('div')

  6. modal.innerHTML= `login modal`

  7. modal.style.display='none'

  8. document.body.appendChild(modal)

  9. }

  10. return modal

  11. }

  12. }()


  13. document.getElementById('loginBtn').onclick=()=>{

  14. const loginModal=createModal()

  15. loginModal.style.display='block'

  16. }

当我多次点击登录按钮的时候,login modal只会创建一次。

createModal这个函数的职责还不够单一,要想让单例模式应用到很多地方,就要把这部分的逻辑抽出来。

 
   
   
 
  1. const getSingle=function(fn){

  2. var res=null

  3. return function(){

  4. return res||(res= fn.apply(this,arguments))

  5. }

  6. }

然后我们再实现那个需求:

 
   
   
 
  1. const createLoginLayer=function(){

  2. const modal=document.createElement('div')

  3. modal.innerHTML= `login modal`

  4. modal.style.display='none'

  5. document.body.appendChild(modal)

  6. return modal

  7. }


  8. document.getElementById('loginBtn').onclick=()=>{

  9. const createSingleLoginModal=getSingle(createLoginLayer)

  10. const loginModal=createSingleLoginModal()

  11. loginModal.style.display='block'

  12. }

如果还需要构造什么其他的单例组件:

 
   
   
 
  1. const createA=function(){

  2. // .....

  3. }


  4. const createSingleA=getSingle(createA)

  5. createSingleA()

这样就符合单一原则啦~~

当然 getSingle()也不仅仅局限于创建dom,比如给元素绑定事件等功能上也可以达到一样的效果。但!不要觉得好用节省开销就处处使用这个函数,因为闭包会占用内存,进行调试也不好操作,所以有必要才可以用,不可画蛇添足。

最近在看《js设计模式与开发实践》,本文是我对此书的一些概括与扩展。下一篇文章写策略模式~~