设计模式--单例模式

165 阅读4分钟

1.定义

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

2. 实现一个单例模式

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

const single = function (name) {
  this.name = name;
  this.instance = null;
};

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

const a = single.getInstance("coderwhh");
const b = single.getInstance("coderwhh1");
// true
console.log(a === b);

在此通过single.getInstance来获取single类的唯一对象,虽然方式简单但是增加了这个类的不透明性。single这个类的使用者必须知道这是一个单例类和以往通过new Function的方式来获取对象不同,这里必须使用single.getInstance来获取对象。

3.透明的单例模式

我们的目标是实现一个“透明”的单例类,用户在使用这个类创建这个对象时可以像使用其他任何普通类一样。 eg:使用createDiv单例类在页面中创建唯一的div节点

const createDiv = (function () {
  var instance = null;
  var createDiv = function (html) {
    if (instance) {
      return instance;
    }
    this.html = html;
    this.init();
    return (instance = this);
  };
  createDiv.prototype.init = function () {
    const div = document.createElement("div");
    div.innerHTML = this;
    document.body.appendChild(div);
  };
  return createDiv;
})();

const a = new createDiv("coderwhh");
const b = new createDiv("coderwhh1");
// true
console.log(a === b);

观看上面:将一个构造函数赋值给createDiv。这个构造函数做了两件事情:1. 创建对象和执行初始化init方法 2.保证只有一个对象。假如一天我们需要利用这个类创建复数个div即将这个单例类变成为一个普通类,那次此时我们就不得不改写createDiv构造函数,将控制创建唯一对象的那一段代码去掉。

4.用代理实现单例模式

首先在createDiv构造函数中将负责管理单例的代码移除,使其成为一个普通创建div的类

const createDiv = function (html) {
  this.html = html;
  this.init();
};

createDiv.prototype.init = function () {
  const div = document.createElement("div");
  div.innerHTML = this.html;
  document.body.appendChild(div);
};

const ProxySingleCreateDiv = (function () {
  let instance;
  return function (html) {
    if (!instance) {
      instance = new createDiv(html);
    }
    return instance;
  };
})();

const a = new ProxySingleCreateDiv("coderwhh");
const b = new ProxySingleCreateDiv("coderwhh1");
// true
console.log(a === b);

通过引入代理类的方式,同样可以完成单例模式编写,这样管理单例的逻辑就移到了代理类ProxySingleCreateDiv中去,createDiv就变成为了一个普通的类。

5.JavaScript中的单例模式

全局变量不是单例模式,但是在JavaScript开发中,经常会把全局变量当成单例来使用,但是全局变量很容易造成命名空间的污染,也随时可能被别人覆盖,作为开发者我们必须减少全局变量的使用。即使需要,也要将污染降到最低。

  • 使用闭包封装私有变量
const user = (function () {
  const __name = "coderwhh";
  return {
    getUserName: function () {
      return __name;
    },
  };
})();

6.惰性单例

惰性单例指的是在需要的时候才创建对象实例,eg:页面上也一个弹窗,这个弹窗的触发条件便是通过点击某个按钮然后显示,很显然这个弹窗在页面是唯一的,并且在页面不一定总是展示,所以可以在点击触发按钮时再来创建这个弹窗。

const 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 () {
  const loginLayer = createLoginLayer();
  loginLayer.style.display = "block";
};

上面的例子存在着一个明显的弊端:1.违反了单一职责原则,创建对象和管理单例的逻辑都放在createLoginLayer对象的内部 2.如果下次创建页面中唯一的iframe,span,又或者是其他元素,那就必须将上面的代码重写一遍。

7. 通用的惰性单例模式

针对上面存在的问题我们先将上面不变的部分给隔离出来。

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

将如何管理单例的逻辑从原来的代码中抽离出来,封装到gteSingle函数中,创建对象的方法fn被当成参数动态的传入getSingle函数

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

接下来可以创建div,iframe标签试一试

// 创建弹框
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";
};

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

上述代码将创建实例对象的职责和管理单例的职责分别放置在了两个方法中,这两个方法可以独立的变化而不影响。当两者连接在一起的时候就完成了创建唯一实例对象的功能。