定义

单例模式的定义:确保一个类只有一个实例,并且提供访问这个实例的全局访问点。

好处

对于一些场景,全局只需要一个实例即可,单例模式可以复用已产生的对象,避免频繁的创建与销毁对象。

使用场景

上面提到了单例模式确保一个类只会产生一个对象,那么使用场景上也会紧紧围绕这一条规则,所使用的实例会保持唯一性。前端范围内,例如浏览器的window对象、某些登录弹窗等等。

实现

JavaScript在ES6出来前没有类的概念,普通函数就能够充当构造函数来产生对象

function Person(name, age) {
  this.name = name;
  this.age = age;
}

const p = new Person('ztq', 18)

那么使用ES5来实现单例模式可能就和其他静态语言实现单例模式就会有很大的差异

普通单例模式

思路就是就变量来标志是否已经创建过变量,如果已经创建过变量,那么在下一次返回的时候直接返回之前创建的变量。

ES5实现

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

Person.prototype.getName = function () {
  return this.name;
};

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

const p1 = Person.getInstance("ztq1");
const p2 = Person.getInstance("ztq2");

console.log(p1)  // Person { name: 'ztq1', instance: null }
console.log(p2)  // Person { name: 'ztq1', instance: null }
console.log(p1 === p2);  // true

ES6实现

class Person {
  constructor(name) {
    this.name = name;
    this.instance = null;
  }
  static getInstance(name) {
    if (!this.instance) {
      this.instance = new Person(name);
    }
    return this.instance;
  }
}

const p1 = Person.getInstance("ztq1");
const p2 = Person.getInstance("ztq2");

console.log(p1); // Person { name: 'ztq1', instance: null }
console.log(p2); // Person { name: 'ztq1', instance: null }
console.log(p1 === p2); // true

透明单例模式

上面创建对象的方式并不是通过使用new操作符的方式,下面转换下思路,使用构造函数的方式来创造单例对象。

ES5实现

const Person = (function () {
  let instance = null;
  return function (name) {
    if (!instance) {
      this.name = name;
      instance = this;
    }
    return instance;
  };
})();

const p1 = new Person("ztq1");
const p2 = new Person("ztq2");

上面利用闭包的特性来保存实例,这种方式可以使用new操作符配合构造函数来生产实例了

ES6实现

class Person {
  static instance = null;
  constructor(name) {
    this.name = name;
    if (!Person.instance) {
      Person.instance = this;
    }
    return Person.instance;
  }
}

const p1 = new Person("ztq1");
const p2 = new Person("ztq2");

利用类的静态变量来标记是否已经生成过单例对象,如果已经生成的话,直接在构造函数里面返回之前已经生成的对象。

通用惰性单例模式

首先解释下什么是惰性单例,它指的是实例只有在使用的时候才会进行创建,而不会提前创建好实例。
假设有这么一种场景,用户在点击登录的时候需要弹出登录弹窗,但是在当有登录弹窗的情况下再次点击登录按钮就不会弹出新的弹窗。因为需要保证登录弹窗的唯一性,所以可以使用单例模式来进行创建。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Singleton</title>
  </head>
  <body>
    <button id="login">登录</button>
  </body>
  <script>
    const createLoginLayer = (function () {
      let div;
      return function () {
        if (!div) {
          div = document.createElement("div");
          div.innerHTML = "登录弹窗";
          div.style.display = "none";
          document.body.appendChild(div);
        }
        return div;
      };
    })();

    document.querySelector("#login").onclick = function () {
      const loginLayer = createLoginLayer()
      loginLayer.style.display = "block";
    };
  </script>
</html>

假设页面中有其它的唯一性要求,例如页面中限制只能有一个iframe,那么我们就还需要照葫芦画瓢,再写一次相似的逻辑。

const createIframe = (function () {
  let iframe;
  return function () {
    if (!iframe) {
      iframe = document.createElement("iframe");
      iframe.style.display = "none";
      document.body.appendChild(iframe);
    }
    return iframe;
  };
})();

结合单例模式的定义以及前面的代码示例,我们可以把变化的内容作为变量,进而形成管理单例对象的模板。

const Singleton = (function(){
  let instance
  return function() {
    if (!instance) {
      instance = xxx
    }
    return instance
  }
})()

简单来说,就是利用闭包的特性来标记是否创建过对象,如果已经创建,下次直接返回创建好的对象。
下面将逻辑抽离出来,整理成函数

function getSingle(fn) {
  let result;
  return function () {
    if (!result) {
      result = fn.apply(this, arguments);
    }
    return result;
  };
}

这样无论是创建div还是创建iframe元素,都可以通过getSingle来获取单例对象。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Singleton</title>
  </head>
  <body>
    <button id="login">登录</button>
  </body>
  <script>
    const createLoginLayer = (function () {
      let div;
      return function () {
        div = document.createElement("div");
        div.innerHTML = "登录弹窗";
        div.style.display = "none";
        document.body.appendChild(div);
        return div;
      };
    })();

    const createSingleLoginLayer = getSingle(createLoginLayer)
    document.querySelector("#login").onclick = function () {
      const loginLayer = createSingleLoginLayer()
      loginLayer.style.display = "block";
    };
  </script>
</html>

参考

  • <<JavaScript设计模式与开发实践>> - 曾探

Tqing
112 声望16 粉丝