美文网首页让前端飞
被问到设计模式不要怂:一文读懂单例模式

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

作者: mytac | 来源:发表于2020-03-21 19:03 被阅读0次

    如果觉得这里显示不好,请直接阅读我的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设计模式与开发实践》,本文是我对此书的一些概括与扩展。下一篇文章写策略模式~~

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

    那屋水泡

    相关文章

      网友评论

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

        本文链接:https://www.haomeiwen.com/subject/tfpuyhtx.html