让前端飞

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

2020-03-21  本文已影响0人  mytac

如果觉得这里显示不好,请直接阅读我的github原文

何为单例

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

如何实现

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

1. flag

function A(name){
    this.name=name
}

A.instance=null

A.getInstance=function(name){
 if(!this.instance){
     this.instance=new A(name)
 }
 return this.instance
}

2. 闭包

将上例getInstance改写:

A.getInstance=(function(name){
    var instance=null
    return function(name){
        if(!instance){
            instance=new A(nanme)
        }
        return instance
    }
})()

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

3. 闭包+同名覆盖

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

var A=(function(){ // 注意用var
    var instance=null
    var A=function(){
        if(instance) return instance;
        this.init()
        return instance=this
    }
    A.prototype.init=function(){
        // do something
    }
})()

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

4. 使用代理类

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

var A=function(name){
    this.name=name
    this.init()
}

A.prototype.init=function(){
    // do something
 }

 // 创建代理类
 var P=(function(){
     var instance=null
     return function(name){
         if(!instance){
            instance=new A(name)
         }
         return instance
     }
 })()

 const p1=new P('aaa')
 const p2=new P('bbb')
 alert(p1===p2) // true

在JS中的应用

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

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

1.命名空间

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

const namespace={
    a:function(){},
    b:{
        c:()=>{}
    }
}

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

const namespace={
    a:function(){},
    b:{
        c:()=>{}
    }
}

Object.freeze(namespace)
namespace.d='ddddd' // 不能扩展
namespace.b='bbbbbb' // 不能修改属性
namespace.b.c='cccccc' // 但内嵌的对象可以修改属性和扩展
console.log(namespace)
/* 输出
a: ƒ ()
b: {c: "cccccc"}
*/

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

Object.freeze(namespace)
Object.defineProperty(namespace.b,'c',{
    writable:false,
    configurable:false
})
namespace.d='ddddd' // 不能扩展
namespace.b='bbbbbb' // 不能修改属性
namespace.b.c='cccccc' // 内嵌的对象也不可被修改
console.log(namespace)
/* 输出
a: ƒ ()
b: {c: ƒ}
*/

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

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

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

var namespace=(function(){
    const _a=function(){}
    const _b={
        c:()=>{}
    }
    return {
        getNameSpace:function(){
            return ({
                a:_a,
                b:_b
            })
        }
    }
})()

Object.freeze(namespace)
namespace.getNameSpace='1111'
console.log(namespace.getNameSpace())
/*输出:
a: ƒ ()
b: {c: ƒ}
*/

惰性单例

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

function A(name){
    this.name=name
}

A.instance=null

A.getInstance=function(name){
 if(!this.instance){
     this.instance=new A(name)
 }
 return this.instance
}

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

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

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

const createModal=function(){
    let modal=null
    return function(){
        if(!modal){
            modal=document.createElement('div')
            modal.innerHTML= `login modal`
            modal.style.display='none'
            document.body.appendChild(modal)
        }
        return modal
    }
}()

document.getElementById('loginBtn').onclick=()=>{
    const loginModal=createModal()
    loginModal.style.display='block'
}

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

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

const getSingle=function(fn){
    var res=null
    return function(){
        return res||(res= fn.apply(this,arguments))
    }
}

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

const createLoginLayer=function(){
    const modal=document.createElement('div')
    modal.innerHTML= `login modal`
    modal.style.display='none'
    document.body.appendChild(modal)
    return modal
}

document.getElementById('loginBtn').onclick=()=>{
    const createSingleLoginModal=getSingle(createLoginLayer)
    const loginModal=createSingleLoginModal()
    loginModal.style.display='block'
}

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

const createA=function(){
    // .....
}

const createSingleA=getSingle(createA)
createSingleA()

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

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

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

最后请大家关注我的订阅号获得更加及时的推送~

那屋水泡
上一篇下一篇

猜你喜欢

热点阅读