一、单例模式(Singleton Pattern)
1.概念介绍
单例模式(Singleton Pattern)的思想在于保证一个特定类仅有一个实例,即不管使用这个类创建多少个新对象,都会得到与第一次创建的对象完全相同。
它让我们能将代码组织成一个逻辑单元,并可以通过单一变量进行访问。
单例模式有以下优点:
-
用来划分命名空间,减少全局变量数量。
-
使代码组织的更一致,提高代码阅读性和维护性。
-
只能被实例化一次。
但在JavaScript中没有类,只有对象。当我们创建一个新对象,它都是个新的单例,因为JavaScript中永远不会有完全相等的对象,除非它们是同一个对象。
因此,我们每次使用对象字面量创建对象的时候,实际上就是在创建一个单例。
let a1 = { name : 'a1' };
let a2 = { name : 'a2' };
a1 === a2; // false
a1 == a2; // false
这里需要注意,单例模式有个条件,是该对象能被实例化,比如下面这样就不是单例模式,因为它不能被实例化:
let a1 = {
b1: 1, b2: 2,
m1: function(){
return this.b1;
},
m2: function(){
return this.b2;
}
}
new a1(); // Uncaught TypeError: a1 is not a constructor
下面展示一个单例模式的基本结构:
let Singleton = function (name){
this.name = name;
this.obj = null;
}
Singleton.prototype.getName = function(){
return this.name;
}
function getObj(name){
return this.obj || (this.obj = new Singleton(name));
}
let g1 = getObj('g1');
let g2 = getObj('g2');
g1 === g2; // true
g1 == g2; // true
g1.getName(); // 'g1'
g2.getName(); // 'g2'
从这里可以看出,单例模式只能实例化一次,后面再调用的话,都是使用第一次实例化的结果。
2.应用场景
单例模式只允许实例化一次,能提高对象访问速度并且节约内存,通常被用于下面场景:
-
需要频繁创建再销毁的对象,或频繁使用的对象:如:弹窗,文件;
-
常用的工具类对象;
-
常用的资源消耗大的对象;
场景案例
这里我们要用单例模式,创建一个弹框,大概需要实现:元素值创建一次,使用的时候直接调用。
因此我们这么做:
// 闭包
let Modal = (function() {
let modal = null;
return function() {
if (!modal) {
modal = document.createElement("div");
modal.innerHTML = "this is modal!";
modal.style.display = "none";
modal.id = "modal";
document.body.appendChild(modal);
}
return modal;
};
})();
document.getElementById("open").addEventListener("click", () => {
let modal = new Modal();
modal.style.display = "block";
});
document.getElementById("close").addEventListener("click", () => {
let modal = new Modal();
modal.style.display = "none";
});
class Modal {
constructor() {
this.dom = null;
if (!Modal.instance) {
this.dom = document.createElement("div");
this.dom.innerHTML = "this is modal";
this.dom.id = "modal";
this.dom.style.display = "none";
this.dom.show = this.show.bind(this);
this.dom.hide = this.hide.bind(this);
document.body.appendChild(this.dom);
Modal.instance = this.dom;
}
return Modal.instance;
}
show() {
this.dom.style.display = "block";
}
hide() {
this.dom.style.display = "none";
}
}
const btnShow = document.getElementById("open");
const btnHide = document.getElementById("close");
btnShow.addEventListener("click", () => {
let modal = new Modal();
modal.show();
});
btnHide.addEventListener("click", () => {
let modal = new Modal();
modal.hide();
});
实现
1. 利用对象实例化操作符new
由于JavaScript中没有类,但JavaScript有 new语法来用构造函数创建对象,并可以使用这种方法实现单例模式。
当使用同一个构造函数以 new操作符创建多个对象,获得的是指向完全相同的对象的新指针。
通常我们使用 new操作符创建单例模式的三种选择,让构造函数总返回最初的对象:
-
使用全局对象来存储该实例(不推荐,容易全局污染)。
-
使用静态属性存储该实例,无法保证该静态属性的私有性。
function L(name){
if(typeof L.obj === 'object'){
return L.obj;
}
this.name = name;
L.obj = this;
return this;
}
let a1 = new L('a1');
let a2 = new L('a2');
a1 === a2 ; // true
a1 == a2 ; // true
缺点:多添加了obj属性,并且obj是公开的,容易被修改,若L对象上存在obj对象,那么就会被覆盖,
那么我们可以利用闭包将该实例包裹,保证实例是私有性并不受外界修改,也不会覆盖原有的属性对象。
我们这通过重写上面的方法,加入闭包:
function L(name){
let instance;
this.name = name;
instance = this; // 1.存储第一次创建的对象
L = function(){ // 2.修改原来的构造函数
return instance;
}
}
let a1 = new L('a1');
let a2 = new L('a2');
a1 === a2 ; // true
a1 == a2 ; // true
当我们第一次调用构造函数,像往常一样返回this,而后面再调用的话,都将重写构造函数,并访问私有变量 obj并返回。
2. 利用es6 class实现
class Singleton {
instance = null;
static getInstance() {
if (!this.instance) {
this.instance = new Singleton();
}
return this.instance;
}
}
const s1 = Singleton.getInstance();
const s2 = Singleton.getInstance();
console.log(s1 === s2); // true
缺点:在多人协作的团队中,无法保障所有人一定不会直接实例化Singleton对象
基于此,就出现了下面的方案:
function Person() {
}
function Singleton(className) {
let instance = null;
return class {
constructor(...args) {
if (!instance) {
instance = new className(...args);
}
return instance;
}
}
}
const createSingleton = Singleton(Person);
const p1 = new createSingleton();
const p2 = new createSingleton();
console.log(p1 === p2); // true
但是这种方案,也会存在问题,比如我createSingleton之前加入以下代码:
createSingleton.prototype.paly = function() {
console.log('play');
}
此时,你用p1、p2去调用play函数时,比如,我用p1去调用play就会报错: p1.paly is not a function, 就如下截图所示:
那么,有没有方法解决这种问题了,这里可以告诉你的是,有,咱们可以用es6代理来处理,接下来我们将Singleton改造成如下代码:
function Singleton(className) {
let instance = null;
return new Proxy(className, {
// target 就是上面的className对象,args就是其对应的参数
construct(target, args) {
if (!instance) {
instance = new target(...args);
}
return instance;
}
});
}
这样,就能解决了上面的几个问题,这里就节点列举这几种方式,其实还有很多种方式,具体可以根据业务场景来实现,答题思路就是这样的了
参考资料
- 《JavaScript Patterns》
- 《JavaScript高级程序设计》
本文使用 文章同步助手 同步