JavaScript设计模式之单例模式

作者: tww844475003 分类: 前端开发 发布时间: 2021-06-05 18:32

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

单例模式是一种常用的模式,有些对象往往只需要一个,比如:线程池、全局缓存、浏览器中的 window 对象等。在 Javascript 开发中,单例模式的用途同样非常广泛,比如做登录弹框,它在当前页面是唯一的,无论单击多少次,都只会创建一次,这就非常适合用单例模式来创建。
  • 单例模式
var Singleton = function(name) {
  this.name = name;
}
Singleton.prototype.getName = function() {
  console.log(this.name);
}
Singleton.getInstance = (function() {
  var instance = null;

  return function(name) {
    if (!instance) {
      instance = new Singleton(name);
    }

    return instance;
  }
})();

var a = Singleton.getInstance('JavaScript');
var b = Singleton.getInstance('vue.js');

console.log(a === b); // true

代码通过 Singleton.getInstance 来获取 Singleton 类的唯一对象, 这种实现相对简单,但也存在一个问题,增加了这个类的“不透明性”,Singleton 类的使用者必须知道这是一个单例类,跟以往通过 new xxx 的方式来获取对象不同。

  • 透明的单例模式

实现一个“透明”的单例类,用户从这个类中创建对象的时候,可以像使用其他任何普通类一样。

var Singleton = (function() {
  var instance = null;

  var Singleton = function(name) {
    this.name = name;

    if (instance) {
      return instance;
    }

    return instance = this;
  }

  Singleton.prototype.getName = function() {
    console.log(this.name);
  }

  return Singleton;
})();

var a = new Singleton('JavaScript');
var b = new Singleton('vue.js');

console.log(a === b); // true
  • 用代理实现单例模式
var Singleton = function(name) {
  this.name = name;
}
Singleton.prototype.getName = function() {
  console.log(this.name);
}

var ProxySingleton = (function() {
  var instance = null;

  return function(name) {
    if (!instance) {
      instance = new Singleton(name);
    }

    return instance;
  }
})();

var a = new ProxySingleton('JavaScript');
var b = new ProxySingleton('vue.js');

console.log(a === b); // true
  • 惰性单例

惰性单例是指在需要的时候才创建对象实例。惰性单例这种技术在实际开发中非常有用,即只有在调用的时候才被创建,而不是在页面的加载好的时候就创建。

Singleton.getInstance = (function() {
  var instance = null;

  return function(name) {
    if (!instance) {
      instance = new Singleton(name);
    }

    return instance;
  }
})();

不过这种基于类的单例模式在 JavaScript 中并不适用,下面我们以登录弹框为例,介绍与全局变量结合实现惰性的单例。

<script>
  var loginMsgbox = (function() {
    var div = document.createElement('div');

    div.innerHTML = '登录框';
    div.style.display = 'none';
    document.body.appendChild(div);
    
    return div;
  })();

  document.getElementById('loginBtn').onclick = function() {
    loginMsgbox.style.display = 'block';
  }
</script>

这种方式会有一个问题,如果用户进入压根不需要进行登录操作,提前创建好 DOM 节点,有点浪费资源。

<button id="loginBtn">登录</button>
<script>
  var loginMsgbox = function() {
    var div = document.createElement('div');

    div.innerHTML = '登录框';
    div.style.display = 'none';
    document.body.appendChild(div);
    
    return div;
  };

  document.getElementById('loginBtn').onclick = function() {
    var login = loginMsgbox();

    login.style.display = 'block';
  }
</script>

现在虽然达到了惰性目的,但失去了单例的效果。加个变量来判断是否已经创建过登录弹框。

<button id="loginBtn">登录</button>
<script>
  var loginMsgbox = (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 login = loginMsgbox();

    login.style.display = 'block';
  }
</script>
  • 通用的惰性单例

上面我们完成了一个可用的惰性单例,但是也存在一些问题

  1. 这段代码仍然是违反单一职责原则的,创建对象和管理单例的逻辑都放在 loginMsgbox 对象内部。
  2. 如果我们下次需要创建页面中唯一的一个注册弹框,就又得把 loginMsgbox 代码重新复制一遍。

需要达到通用,就得经过封装,需要把不变的部分隔离出来。

<button id="loginBtn">登录</button>
<button id="registerBtn">注册</button>
<script>
  // 封装单例模式
  var getSingle = function(fn) {
    var result;

    return function() {
      return result || (result = fn.apply(this, arguments));
    }
  }

  // 登录
  var loginMsgbox = function() {
    var dom = document.createElement('div');

    dom.innerHTML = '登录弹框';
    dom.style.display = 'none';
    document.body.appendChild(dom);

    return dom;
  }
  var createLoginMsgbox = getSingle(loginMsgbox);
  document.getElementById('loginBtn').onclick = function() {
    var login = createLoginMsgbox();

    login.style.display = 'block';
  }

  // 注册
  var registerMsgbox = function() {
    var dom = document.createElement('div');

    dom.innerHTML = '注册弹框';
    dom.style.display = 'none';
    document.body.appendChild(dom);

    return dom;
  }
  var createRegisterMsgbox = getSingle(registerMsgbox);
  document.getElementById('registerBtn').onclick = function() {
    var register = createRegisterMsgbox();

    register.style.display = 'block';
  }
</script>

单列模式用途远不止创建对象,比如给 li 列表绑定 click 事件,如果是通过 ajax 动态往列表里追加数据,在使用事件代理的前提下(事件委托不会存在),click 事件实际上只需要在第一次渲染列表的时候被绑定一次,但是又不想去判断当前是否是第一次渲染列表。

<div id="div1">div1</div>
<script>
  // 封装单例模式
  var getSingle = function(fn) {
    var result;

    return function() {
      return result || (result = fn.apply(this, arguments));
    }
  }

  // 绑定事件
  var bindEvent = getSingle(function() {
    console.log('绑定事件')
    document.getElementById('div1').addEventListener = function() {
      alert('click');
    }

    return true;
  })

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

  render();
  render();
</script>

前端开发那点事
微信公众号搜索“前端开发那点事”

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注