JavaScript 单例模式

4,460 阅读6分钟
  • 设计模式就是在某种场合下对特定问题的简洁而又优雅的解决方案
  • 单例模式在JS中尤为突出(每个对象字面量都可以看做是一个单例~)

Singleton 单例模式(单态模式)

1.定义 & 实现思路

确保(一个类)仅有一个实例,并提供全局访问。

2.JavaScript 中单例模式的实现

  • 在JavaScript里,单例作为一个命名空间提供者,从全局命名空间里提供一个唯一的访问点来访问该对象。

  • 在 js 开发中,我们经常会把全局变量当成单例来使用

2.1 最简单的对象字面量

再看单例模式的定义:【确保(一个类)仅有一个实例,并提供全局访问】

var Sinleton = {
  attr: 1,
  show() {
    return this.attr
  }
}
var t1 = Sinleton
var t2 = Sinleton
console.log(t1 === t2) // true

这样创建的对象 Sinleton ,

  1. 对象 Sinleton 确实是独一无二的。
  2. 如果 Sinleton 变量被声明在全局作用域下,那么我们可以在代码中的任何位置使用这个变量。

这样就满足了单例模式的两个条件。

优点:简单且实用

缺点:

  1. 没有什么封装性,所有的属性方法都是暴露的。对于一些需要使用私有变量的情况就显得心有余而力不足了
  2. 对于 this 的问题也有一定弊端。

2.2 构造函数内部判断

function Singleton(name, age) {
  if(Singleton.unique) {
    return Singleton.unique
  }
  this.name = name
  this.age = age
  Singleton.unique = this
}
var t1 = new Singleton('cherish', 18)
var t2 = new Singleton('silence', 18)
console.log(t1 === t2) // true

缺点:提出一个属性来做判断,但是也没有安全性,一旦外部修改了Construct的unique属性,那么单例模式也就被破坏了。

2.3 使用闭包实现单例模式

  • 使用闭包将创建了的单例缓存起来
var Singleton = (function() {
   function Construt() {
  }
  return new Construt()
})()

var t1 = Singleton
var t2 = Singleton
console.log(t1 === t2) // true

与对象字面量方式类似。不过相对而言更安全一点,当然也不是绝对安全。 如果希望会用调用 single() 方式来使用,那么也只需要将内部的 return 改为

var Singleton = (function() {
  var constance = null
  return function Construt() {
    if(!constance) {
      constance = this
    }
    return constance
  }
})()

var t1 = new Singleton('cherish', 18)
var t2 = new Singleton('silence', 18)
console.log(t1 === t2) // true

3. 传统方法实现单例模式

  重点:保证一个类只有一个实例.
1. 先判断实例存在与否,存在直接返回,如果不存在就创建了再返回.
   一个类拥有一个获得该实例的 getInstance 静态方法,返回单例实例对象的引用,保证永远是同一个实例对象。
2. 确保一个类只有一个实例对象。将该类的构造函数定义为私有方法,避免其他函数使用该构造函数来实例化对象,只通过该类的静态方法来得到该类的唯一实例.  

3.1 实现一个简单的单例模式(不透明的)

class Singleton {
  constructor(name) {
    this.name = name;
    this.instance = null;
  }
  static getInstance(name) {
    if (!this.instance) {
      this.instance = new Singleton(name);
    }
    return this.instance;
  }
  getName() {
    return this.name;
  }
}
// getInstance:闭包 + 私有变量 的实现
Singleton.getInstance = (function () {
  let instance = null;
  return function (name) {
    if (!instance) {
      instance = new Singleton(name);
    }
    return instance;
  };
})();

缺点:

  1. 不透明。使用者必须研究代码的实现,知道要通过 Singleton.getInstance() 来获取单例对象。
  2. 不能保证Singleton类只有一个实例对象。使用者依然可以通过 new 关键字实例化对象。

3.2 实现透明的的单例模式

var Singleton = (function () {
  var instance = null;
  var createDiv = function (html) {
    if (!instance) {
      this.init(html);
    }
    return (instance = this);
  };
  createDiv.prototype.init = function (html) {
    var divObj = document.createElement("div");
    divObj.innerHTML = html;
    console.log(document.body.appendChild, divObj, html);
    document.body.appendChild(divObj);
  };
  return createDiv;
})();
var a = new Singleton("<span>aaa</span>");

3.3 +缓存代理,组合后,实现可复用的单例模式

功能拆解: 把 CreateDiv 独立出来, 而负责管理单例的逻辑移到代理类中,保证只有一个对象。 这样一来,CreateDiv 就变成了一个普通的类,它跟 代理 组合起来可以达到单例模式的效果。

class Creatediv {
  constructor(html) {
    this.init(html);
  }
  init(html) {
    const divObj = document.createElement("div");
    divObj.innerHTML = html;
    document.body.appendChild(divObj);
  }
}
const SingletonProxy = (function () {
  let instance = null;
  return function (html) {
    if (!instance) {
      instance = new Creatediv(html);
    }
    return instance;
  };
})();
const a = new SingletonProxy("<span>我是一个span标签1</span>");
const b = new SingletonProxy("<span>我是一个span标签2</span>");
console.log(a === b);

3.4 单一职责--登陆浮窗优化

// 调用才创建,而非页面加载好就创建
// 与全局变量结合
var getSingle = function (fn) {
  let instance = null;
  return function () {
    if (!instance) {
      instance = fn(...arguments)
    }
    return instance;
  };
};
var createLoginLayer = function () {
  loginLayerObj = document.createElement("div");
  loginLayerObj.innerHTML = "<span>i am a loginLayObj</span>";
  loginLayerObj.style.display = "none";
  document.body.appendChild(loginLayerObj);
  return loginLayerObj
}
var btnObj = document.querySelector("#loginBtn");
btnObj.addEventListener("click", () => {
  const loginLayerObj = getSingle(createLoginLayer);
  loginLayerObj.style.display = "block";
});

4.前端应用场景

  • 浏览器的 window 对象。在 JavaScript 开发中,对于这种只需要一个的对象,往往使用单例实现。

  • 遮罩层、登陆浮窗等。

5.其他应用场景

单例模式应用的场景一般发现在以下条件下:

(1)资源共享的情况下,避免由于资源操作时导致的性能或损耗等。如上述中的日志文件,应用配置。

(2)控制资源的情况下,方便资源之间的互相通信。如线程池等。

如:

  • Windows 的 Task Manager(任务管理器)、Recycle Bin(回收站)。

  • 网站的计数器,一般采用单例模式实现,否则难以同步。(计数器会告诉你关于你的网站的某个特定页面上的访问次数)。

  • 线程池。多线程的线程池的设计一般采用单例模式,这是由于线程池要方便对池中的线程进行控制。

  • 全局缓存等。

6. 避免/降低全局变量的命名污染

全局变量容易造成命名空间污染。在大中型项目中,如果不加以限制和管理,程序中可能存在很多这样的变量。JavaScript 中的变量也很容易被不小心覆盖。
作为普通的开发者,我们有必要尽量减少全局变量的使用,即使需要,也要把它的污染降到最低。以下几种方式可以相对降低全局变量带来的命名污染。

开发中我们避免全局变量污染的通常做法如下:

  • 全局命名空间
  • 使用闭包 它们的共同点是都可以定义自己的成员、存储数据。 区别是全局命名空间的所有方法和属性都是公共的,而闭包可以实现方法和属性的私有化。

6.1 使用命名空间

6.1.1 对象字面量

var nameSpace = {
  a: function () {
    console.log("a");
  },
  b: function () {
    console.log("b");
  },
};

6.1.2.动态地创建命名空间

var myApp = {};
myApp.nameSpace = function (name) {
  var parts = name.split(".");
  let current = myApp;
  for (let i in parts) {
    if (!current[parts[i]]) {
      current[parts[i]] = {};
    }
    current = current[parts[i]];
  }
};
myApp.nameSpace("cherish.name");
myApp.nameSpace("silence.age");
// 上述代码等价于:
var MyApp = {
  cherish: {
    name: {},
  },
  silence: {
    age: {},
  },
};

6.2 使用闭包封装私有变量

var nameSpace = (function () {
  let _name = 'cherish'
  return {
    getInfo() {
      return _name
    }
  }
})()
console.log(nameSpace.getInfo());

7. 总结

设计模式讲究对象之间的关系的抽象,而单例模式只有自己一个对象.

在开发中经常会使用中间类,通过它来实现原类所不具有的特殊功能。———— 结合缓存代理,实现可复用的单例模式.

如果严格的只需要一个实例对象的类(虽然JS没有类的概念),那么就要考虑使用单例模式.

使用数据缓存来存储该单例,用作判断单例是否已经生成,是单例模式主要的实现思路.

参考