《JavaScript设计模式与开发实践》——第四章(单例模式)学习记录

单例模式的定义是:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
在JavaScript开发中,单例模式的用同样非常广泛,当我们单击登录按钮的时候,页面中会出现一个登录浮窗,这个浮窗是唯一的,无论单击多少次登录按钮,这个浮窗都只会被创建一次,这个浮窗就很适合用单例模式来创建。

4.1实现单例模式

其实一个单例模式并不复杂,无非就是用一个变量来标志当前是否已经为某个类创建过对象,如果是,则在下一次获取该类的实例时,直接返回之前创建的对象:

var Singleton = function(name){
      this.name = name;
    }
    Singleton.instance = null;
    Singleton.prototype.getName = function(name){
      alert(this.name);
    }
    Singleton.getInstance = function(name){
      if(!this.instance){
        this.instance = new Singleton(name)
      }
      return this.instance
    }
    var a = Singleton.getInstance('sven1');
    var b = Singleton.getInstance('sven2');
  
    alert(a === b);//true

或者:

 var Singleton = function(name){
      this.name = name;
    }
    Singleton.prototype.getName = function(name){
      alert(this.name);
    }
    Singleton.getInstance = (function(){
      var instance = null;
       return function(name){
         if(!this.instance){
	         this.instance = new Singleton(name)
	       }
	     return this.instance
       }
    })()
    var a = Singleton.getInstance('sven1');
    var b = Singleton.getInstance('sven2');
  
    alert(a === b);//true

我们通过Singleton.getInstance来获取Singleton类的唯一对象,这种方式相对简单,但是这样写增加了这个了这个类的“不透明性”,Singleton类的使用者必须知道这是一个单例类,跟以往通过new XXX的方式来获取对象不同,这里偏要使用Singleton.getInstance来获取对象。

4.2透明的单例模式

上边虽然已经完成了一个单例模式的编写,但是这段单例代码的意义不大,我们现在的目标是实现一个“透明”的单例类,用户从这个类中创建对象的时候,可以像使用其他任何普通类一样。接下来,我们将使用creatDiv单例类,它的作用是负责在页面中创建唯一的div节点,代码如下:

	var CreateDiv = (function(){
      var instance;
      var CreateDiv = function(html){
        if(instance){
          return instance;
        }
        this.html = html;
        this.init();
        return instance = this;
      }
      CreateDiv.prototype.init = function(){
        var div = document.createElement('div');
        div.innerHTML = this.html;
        document.body.appendChild(div);
      }
      return CreateDiv;
    })();

    var a = new CreateDiv('sven1');
    var b = new CreateDiv('sven2');

    alert(a === b)//true

虽然完成了一个透明的单例类的编写,但它同样有一些缺点。为了把instance封装起来,我们使用了自执行的匿名函数和闭包,并且让这个匿名函数返回真正的Singleton构造方法,这增加了一些程序的复杂度,阅读起来也不是很舒服。
看下现在的Singleton代码:

	var CreateDiv = function(html){
        if(instance){
          return instance;
        }
        this.html = html;
        this.init();
        return instance = this;
      }

这段代码负责了两件事,第一是创建对象和执行初始化init方法,第二是保证只有一个对象。虽然我们还没有学习“单一职责原则”的概念,但是可以明确,这种做法并不是很好。
如果我们某天需要用这个类在页面中创建千千万万的div,既要让这个类从单例类变成一个普通的可产生多个实例的类,那我们必须得改写CreateDiv构造函数,把控制创建唯一对象的那一段去掉,如果要做这样的修改那我可能会很暴躁!

4.3用代理实现单例模式

我们依然使用4.2中的代码,把负责管理单例的代码移除出去,使它成为一个普通的创建div的类:

	var CreateDiv = function(html){
        this.html = html;
        this.init();
      }
      CreateDiv.prototype.init = function(){
        var div = document.createElement('div');
        div.innerHTML = this.html;
        document.body.appendChild(div);
      }
      //接下来引入代理类ProxySingletonCreateDiv
      var ProxySingletonCreateDiv = (function(){
        var instance;
        return function(html){
          if(!instance){
            instance = new CreateDiv(html)
          }
          return instance
        }
      })()


    var a = new ProxySingletonCreateDiv('sven1');
    var b = new ProxySingletonCreateDiv('sven2');

	alert(a === b);//true

通过引入代理的方式,我们同样引入了一个单例模式的编写,跟之前不同的是,现在我们把负责管理单例的逻辑移到了代理类ProxySingletonCreateDiv中。CreateDiv变成了一个普通的类,跟ProxySingletonCreateDiv组合起来可以达到单例模式的效果。(本例也是缓存代理的应用之一)

4.4JavaScript中的单例模式

前面提到的几种单例模式的实现,更多的是比较接近传统面向对象语言中的实现,单例对象从“类”中创建而来。但是JavaScript是一门无类语言,所以生搬单例模式的概念有些多此一举。传统的单例模式是现在JavaScript中并不适用。
在开发中,我们会把全局变量当成单例使用,例如:
var a = {};
当用这种方式创建对象a时,对象a确实是独一无二的。如果a变量被声明在全局作用域里,我们可以在代码中的任何位置使用这个变量。但是全局变量存在很多问题,它很容易造成命名空间污染。在大型项目中,如果不加以限制和管理,程序中可能存在很多这样的变量。一不小心就会被覆盖。
作为普通的开发者,我们要尽量减少全局变量的使用,即使需要,也要把它的污染降到最低。例如以下两种方式,可以做到相对降低全局变量带来的命名污染:

1.使用命名空间
适当的使用命名空间不会杜绝全局变量,但可以减少全局变量的数量。
如下面这种对象字面量的方式:

	var namespace1 = {
      a:function(){
        alert(1);
      },
      b:function(){
        alert(2)
      }
    }

另外我们还可以这样动态地创建命名空间:

	var myApp = {};
    myApp.namespace = function(name){
      var parts = name.split('.');
      var current = myApp;
      for(var i in parts){

        if(!current[parts[i]]){
          current[parts[i]] = {};
        }
        current = current[parts[i]];
      }
    }
    myApp.namespace('event');
    myApp.namespace('dom.style');
    console.dir(myApp);
    // 上述代码等价于:
    var MyApp = {
      event:{},
      dom:{
        style:{}
      }
    }

2.使用闭包封装私有变量
这种方法把一些变量封装在闭包的内部,只暴露一些接口跟外界通信:

	var user = (function(){
      var _name = 'sven',
        _age = 29;

        return {
          getUserInfo:function(){
            return _name + '-' + _age;
          }
        }
    })()
     console.log(user.getUserInfo())//sven-29

我们用下划线来约定私有变量_name和_age,它们被封装在闭包的作用域中,外部是访问不到这两个变量的。

4.5惰性单例

惰性单例指的是在需要的时候才创建对象实例。是单例模式的重点,这种技术在实际开发中非常有用,有用的程度可能超出了我们的想象。
实际上在文章开始的时候,我们已经使用过这种技术了:

 	Singleton.getInstance = (function(){
      var instance = null;
        return function(name){
          if(!this.instance){
          this.instance = new Singleton(name)
        }
        return this.instance
        }
    })()

instance实例对象总是在我们调用Singleton.getInstance的时候才被创建,而不是在页面加载的时候就创建。
不过这是基于“类”的单例模式,前面说过,基于“类”的单例模式在JavaScript中并不适用,下面我们通过WebQQ的登录浮窗来了解全局变量结合实现惰性的单例。
当我们要登录QQ的时候,点击QQ图标会弹出一个登录浮窗,很明显这个浮窗在页面里总是唯一的,不可能出现同时存在两个窗口的情况。
在这里插入图片描述
第一种方法也是我们都会的方法,一开始就创建这个浮窗,先将它隐藏,需要的时候再显示,但是这样可能会有一个问题,也许我们进入webQQ只想看看天气,不想登录,浮窗一开始就创建好就会白白浪费一些DOM节点。
接下来我们用惰性单例来实现一下:

<button id="loginBtn">登录</button>
	var createLoginLayer = (function(){
      var div;
      return function(){
        if(!div){
          div = document.createElement('div');
          div.innerHTML = '我是登录浮窗';
          div.style.display = 'none';
          document.body.appendChild(div);
          console.log("创建浮窗")
        }
        return div;
      }
    })()
    document.getElementById('loginBtn').onclick = function(){
      var loginLayer = createLoginLayer();
      loginLayer.style.display = 'block';
    }

多次点击按钮,我们可以看到控制台只输出了一次"创建浮窗",(这里还缺少一个关闭按钮,点击就隐藏浮窗)

4.6通用的惰性单例

上一节我们完成了一个惰性单例,但是发现它有一些问题:

  1. 这段代码仍然是违反单一职责原则的,创建对象和管理单例的逻辑都放在了createLoginLayer对象内部。
  2. 如果我们下次需要创建页面中唯一的iframe或者别的标签,用来夸域请求数据,就必须得如法炮制,写一份几乎一摸一样的代码。
    我们需要把不变的部分隔离出来,先不考虑创建一个div会让创建一个iframe有多少差异,管理单例的逻辑其实可以完全抽离出来,用一个变量来标志是否创建过对象,如果是,则在下次直接返回这个已经创建好的对象:
 	var getSingle = function(fn){
      var result;
      return function(){
        return result || (result = fn.apply(this,arguments));
      }
    };

接下来将用于创建登录浮窗的方法用参数fn的形式传入getSingle,我们不仅可以传入createLoginLayer,还能传入createIframe等,之后再让getSingle返回一个新函数,并且用一个变量result来保存fn的计算结果,result因为在闭包中,永远不会被销毁,如果result已经被赋值,那么它将返回这个值:

	var createLoginLayer = function(){
      var div = document.createElement('div');
      div.innerHTML = '我是登录浮窗';
      div.style.display = 'none';
      document.body.appendChild(div);
      console.log("创建浮窗");
      return div;
    }

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

再来试一试创建唯一的iframe用于动态加载第三方页面:

	var createIframe = getSingle(function(){
      var iframe = document.createElement('iframe');
      document.body.appendChild(iframe);
      console.log("创建iframe")
      return iframe;
    })
    document.getElementById('loginBtn').onclick = function(){
      var loginLayer = createIframe();
      loginLayer.src = 'http://baidu.com';
    }

这个例子中,我们把创建实例对象的职责和管理单例的职责分别放置在两个方法里,这两个方法可以独立变化互不影响,当它们连接在一起的时候,就完成了创建唯一实例对象的功能。
这种单例模式的用途远不止创建对象,比如我们通常渲染完页面中的一个列表后,接下来要给这个列表绑定click事件,如果是通过ajax动态往列表里追加数据,在使用事件代理的前提下,click 事件实际上只需要在第一次渲染列表的时候被绑定一次,但是我们不想去判断当前是否是第一次渲染列表,如果是借助jQuery,我们通常选择给节点绑定one事件,如果利用getSingle函数,也能达到一样的效果。代码如下:

 	var bindEvent = getSingle(function(){
      console.log("开始绑定事件")
      document.getElementById('div').addEventListener = function(){
        console.log("绑定事件成功")
        alert('click')
      }
      return true;
    })

    var render = function(){
      console.log("开始渲染列表");
      bindEvent();
    }

    render();
    render();
    render();

在这里插入图片描述
可以看到,render函数和bingEvent 函数分辨执行了三次,但div实际上只被绑定了一次事件。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值