# 单例模式

单例模式(Singleton):又被称为单体模式,是只允许实例化一次的对象类。有时我们也用一个对象来规划一个命名空间,井井有条地管理对象上的属性和方法。

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

单例模式是一种常用的模式,有一些对象我们往往只需要一个,比如线程池,全局缓存,浏览器中的 window 对象,sessionStorage 对象,localStorage 对象等。

# 简单实现单例模式

要实现一个标准的单例模式,基本思路是用一个变量来标识当前是否以及为某个类创建过对象,如果是,则下一次获取该类的实例时,直接返回之前创建的对象。

const Singleton = function (name) {
  this.instance = null;
  this.name = name;
};
Singleton.prototype.getName = function () {
  return this.name;
};
Singleton.getInstance = (function (name) {
  let instance = null;
  return function (name) {
    if (!instance) {
      instance = new Singleton(name);
    }
    return instance;
  };
})();
let a = Singleton.getInstance("alice");
let b = Singleton.getInstance("bob");
console.log(a.getName()); // alice
console.log(b.getName()); // alice
console.log(a === b); // true

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

# 用代理实现单例模式

// 首先创建一个普通的生成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 ProxySingletonCreateDiv = (function () {
  let instance = null;
  return function (html) {
    if (!instance) {
      instance = new CreateDiv(html);
    }
    return instance;
  };
})();
// 测试
let a = new ProxySingletonCreateDiv("alice");
let b = new ProxySingletonCreateDiv("bob");
console.log(a === b); // true

# 全局变量与命名空间

全局变量不是单例模式,但在 JavaScript 开发中,我们经常会把全局变量当成单例来使用。 但是全局变量存在很多问题,它很容器造成命名空间污染,在早期没有模块化编程的大中型项目中,如果不加以限制和管理,就很容易让我们的程序存在很多这样的变量。

  1. 使用命名空间

1.1 使用对象字面量,一个对象就是一个模块,就是一个命名空间

const namespace1 = {
  a:function(){
    console.log('a')
  },
  b:function(){
    console.log('b')
  }b
}

1.2 巧妙的动态创建命名空间

const app = {};
app.namespace = function (name) {
  const parts = name.split(".");
  let current = app; // 指针
  let next = null;
  for (i in parts) {
    next = current[parts[i]];
    if (!next) {
      current[parts[i]] = {};
      next = current[parts[i]];
    }
    current = next;
  }
};
app.namespace("event");
app.namespace("dom.style");
console.log(app); // { namespace: [Function], event: {}, dom: { style: {} } }
  1. 使用闭包封装私有变量

这种方法将一些变量封装在闭包的内部,只向外暴露一些接口

const user = (function () {
  var _name = "tom";
  var _age = "18";
  return {
    getUserInfo: function () {
      return _name + "-" + _age;
    }
  };
})();

这里我们约定使用下划线来定义模块内部的私有变量

  1. 使用目前大行其道的模块化

借助于 nodejs 和各种打包器的生态,我们有了模块化,每一个模块都是一个闭包环境

import Cookies from "js-cookie";

const debug = process.NODE_ENV != "production";

function get(key) {
  let value = Cookies.get(key);
  debug && console.log(`cookie.get: ${key} -- ${value}`);
}

function set(key, value) {
  Cookies.set(key, value);
  debug && console.log(`cookie.set: ${key} -- ${value}`);
}

function clear() {
  let cookies = document.cookie.split(";");
  for (var i = 0; i < cookies.length; i++) {
    var cookie = cookies[i];
    var eqPos = cookie.indexOf("=");
    var name = eqPos > -1 ? cookie.substr(0, eqPos) : cookie;
    document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT";
  }
}

export default {
  get,
  set,
  clear
};

# 惰性单例及引用

惰性单例指的是在需要的时候才创建对象示例,实际上我们前面已经这么做了。

现在让我们贴近需求,来创建一个唯一的弹窗实例。

<body>
  <button id="loginBtn">登录</button>
</body>
const createSingleLoginLayer = (function () {
  let div = null;
  return function () {
    if (!div) {
      div = document.createElement("div");
      div.innerHTML = "我是登录浮窗";
      div.style.display = "none";
      document.body.appendChild(div);
    }
    return div;
  };
})();

document.getElementById("loginBtn").addEventListener("click", function () {
  let loginLayer = createSingleLoginLayer();
  loginLayer.style.display = "block";
});

是的,这样已经实现了,但我们还能继续优化,这里需要应用到单一职责原则。

// 通用的惰性单例
const getSingle = function (fn) {
  let result;
  return function () {
    return result || (result = fn.apply(this, arguments));
  };
};
// 具体业务
const createLoginLayer = function () {
  div = document.createElement("div");
  div.innerHTML = "我是登录浮窗";
  div.style.display = "none";
  document.body.appendChild(div);
};

const createSingleLoginLayer = getSingle(createLoginLayer);

document.getElementById("loginBtn").addEventListener("click", function () {
  let loginLayer = createSingleLoginLayer();
  loginLayer.style.display = "block";
});

在这个例子中,我们把创建实例对象和职责和管理单例的职责分别放置在两个方法里,这两个方法可以独立变化,当他们连接在一起的时候,就完成了创建唯一实例对象的功能,我只能说非常的酷 Cool~

又比如这个需求,给一个动态列表里绑定 click 事件。

// 使用jquery
const bindEvent = function () {
  $("div").one("click", function () {
    console.log("div.click");
  });
};
const render = function () {
  console.log("Start Render");
  bindEvent();
};
render();
render();
render();
// 使用getSingle
const bindEvent = getSingle(function () {
  document.getElementById("div").addEventListener("click", function () {
    console.log("div.click");
  });
});
const render = function () {
  console.log("Start Render");
  bindEvent();
};
render();
render();
render();

# 小结

单例模式是一种简单但非常使用的模式,特别是惰性单例模式,在合适的时候才创建对象,并且只创建唯一的一个。

单例模式的模板

// 通用的惰性单例
const getSingle = function (fn) {
  let result;
  return function () {
    return result || (result = fn.apply(this, arguments));
  };
};