javascript设计模式之单例模式

1,376 阅读4分钟

什么是单例模式?

单例模式的定义为:保证一个类仅有一个实例,并提供一个访问它的全局访问点。 

简单来说,单例模式就是把一堆代码,放入到一个逻辑单元里面,可以通过单一的变量来访问。最大的好处就是封装代码,减少全局变量


为什么要使用单例模式?

在我们最开始的写法中。我们通常会这样写:

var btn = document.querySelector('#btn')
btn.onclick = function() {
    render()
}
function render() {
    console.log('render')
}

那么上面的代码有什么问题呢?

最主要的就是全局变量多,难以维护

现在我们开始改进:

我们先把这一堆代码整合到一个对象里面,作为他的属性和方法,只保留一个全局变量。通过预留的入口启动。

var app = {
    btn: document.querySelector('#btn'),
    init: function() {
        this.bind()
    },
    bind: function() {
        var _this = this
        this.btn.onclick = function() {
            _this.render()
        }
    },
    render() {
        console.log('render jirengu.com')
    }
}
app.init()

虽然全局变量可以访问了,但是还是有一个缺点,app里面的属性和方法还是暴露了出来,可以直接用app访问。那我们接下来使用闭包来将部分变量和方法隐藏起来。

var app = (function(){
   var btn = document.querySelector('#btn')
   function bind() {
       btn.onclick = function() {
           render()
       }
   }
   function render() {
       console.log('render jirengu.com')
   }

    return {
        init: function() {
            bind()
        }
    }
})()

app.init()

这种特殊的单例模式也叫模块模式(module pattern)。

那么还有没有改进的方法呢?当然还有,那就是在我们需要的时候在去创建,而不是立即创建实例。代码如下:

var Singleton = (function(){
    var instance
    function createInstance() {
        var btn = document.querySelector('#btn')
        function bind() {
            btn.onclick = function() {
                render()
            }
        }
        function render() {
            console.log('render jirengu.com')
        }
        return {
            init: function() {
                bind()
            }
        }
    }
    return {
        getInstance: function() {
            if(!instance) {
                instance = createInstance()
            }
            return instance
        }
    }
})()

var app = Singleton.getInstance()
app.init()

单例模式的应用场景

当我们访问一个网站的时候,比如访问以前的wepQQ(老版本),当我们点击时候,会有一个弹出层,来让我们去登陆。如下图:


那么,这个唯一的窗口要如何创建呢?

由于我们知道只有当点击登陆按钮的时候,弹层才会出现,所以,我们可以在点击按钮后,惰性的创建一个div弹层。所以js代码如下:

var createLoginLayer = function(){
    var div = document.createElement( 'div' );
    div.innerHTML = '我是登录浮窗';
    div.style.display = 'none'; 
    document.body.appendChild( div );
    return div;};  
document.getElementById( 'loginBtn' ).onclick = function(){ 
 var loginLayer = createLoginLayer();
  loginLayer.style.display = 'block';
 };

可是这样的实现方法有一个缺点就是,当我们每次点击登陆按钮时,都会创建一个新的DIV,当我们点击关闭按钮时候,这个浮层又会消失。这样频繁的触发DOM操作,创建和删除节点明显是不合理的,也是不必要的。

那么我们是否可以设置一个变量来判断是否已经创建过浮层呢?改进后代码如下:

var createLoginLayer = (function(){ 
    var div;  return function(){
     if ( !div ){
        div = document.createElement( 'div' );
         div.innerHTML = '我是登录浮窗'; 
       div.style.display = 'none';
        document.body.appendChild( div );
      }
    return div;
   } })();

document.getElementById( 'loginBtn' ).onclick = function(){
   var loginLayer = createLoginLayer();
   loginLayer.style.display = 'block';
};

这样便实现了只创建一个浮层需求。

可是上面的代码还有一个问题:假如我还有其他的地方要创建单一的浮层,那么,我们要几乎将代码原封不动的写一遍,然后实现这个需求。

那么,我们需要把创建一个div和其他的iframe的公共部分给抽离出来:用一个变量来标志是否创建过对象,如果是,则在下次直接返回这个已经创建好的对象 。

var obj;
if (!obj){
    obj = {};
}

那么我们将这些方法抽离出来,这些逻辑被封装在getSingle 函数内部,创建对象的方法 fn 被当成参数动态传入 getSingle 函数,代码如下:

function getSingle(fn){
    var result;
    return result || (result = fn.call(this,arguments));

}

我们抽离代码后,便可以将创建的逻辑单独拿出来,放入getSingle,我们用result来保存fn的值,由于闭包,所以result永远不回被撤销。

所以代码可以如下所示:

var createLoginLayer = function(){
  var div = document.createElement( 'div' );
    div.innerHTML = '我是登录浮窗';
    div.style.display = 'none';
    document.body.appendChild( div );
    return div;
  };
var createSingleLoginLayer = getSingle(createLoginLayer);
  document.getElementById( 'loginBtn' ).onclick = function(){
   var loginLayer = createSingleLoginLayer();
   loginLayer.style.display = 'block';
};  


总结

首先单例模式的核心是:确保只有一个实例,并提供全局访问。

单例模式是一个简单而且使用的模式,而且惰性的单例模式,是我们可以完成需要的时候再去请求的功能,并且只创建唯一一个。

用好单例模型可以减少DOM的重复渲染,提高浏览器运行的效率。 


参考文献

JavaScript设计模式与开发实践