JavaScript 设计模式——单例模式

185 阅读4分钟

一、单例模式(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;
        }
    });
}

这样,就能解决了上面的几个问题,这里就节点列举这几种方式,其实还有很多种方式,具体可以根据业务场景来实现,答题思路就是这样的了

参考资料

  1. 《JavaScript Patterns》
  2. 《JavaScript高级程序设计》

本文使用 文章同步助手 同步