单例模式

# 单例模式

# 一、什么是单例模式?

单例模式(Singleton Pattern)的思想在于保证一个特定类仅有一个实例,即不管使用这个类创建多少个新对象,都会得到与第一次创建的对象完全相同

单例模式有以下优点:

  • 用来划分命名空间,减少全局变量数量。
  • 使代码组织的更一致,提高代码阅读性和维护性。
  • 只能被实例化一次。

在 JavaScript 中没有类,只有对象。当我们创建一个新对象,它都是个新的单体,因为 JavaScript 中永远不会有完全相等的对象,除非它们是同一个对象。 因此,我们每次使用对象字面量创建对象的时候,实际上就是在创建一个单例。

// 创建了两个新对象
let a = { name: "leo" };
let b = { name: "leo" };

a === b; // false
a == b; // false
1
2
3
4
5
6

# 1.1 看一个最简单的单例模式 👇

let timeTool = {
  name: "处理时间工具库",
  getISODate: function() {},
  getUTCDate: function() {},
};
// 以对象字面量创建对象的方式在JS开发中很常见。
1
2
3
4
5
6

👆 上面的对象是一个处理时间的工具库, 以对象字面量的方式来封装了一些方法处理时间格式。全局只暴露了一个timeTool对象, 在需要使用时, 只需要采用timeTool.getISODate()调用即可。timeTool对象就是单例模式的体现。在 JavaScript 创建对象的方式十分灵活, 可以直接通过对象字面量的方式实例化一个对象, 而其他面向对象的语言必须使用类进行实例化。所以,这里的timeTool就已经是一个实例, 且 ES6 中let 和 const不允许重复声明的特性,确保了timeTool不能被重新覆盖。

# 1.2 惰性单例

采用对象字面量创建单例只能适用于简单的应用场景,一旦该对象十分复杂,那么创建对象本身就需要一定的耗时,且该对象可能需要有一些私有变量和私有方法。

此时使用对象字面创建单例就不再行得通了,我们还是需要采用构造函数的方式实例化对象。下面就是使用立即执行函数和构造函数的方式改造上面的 timeTool 工具库。

let timeTool = (function() {
  let _instance = null;

  function init() {
    //私有变量
    let now = new Date();
    //公用属性和方法
    (this.name = "处理时间工具库"),
      (this.getISODate = function() {
        return now.toISOString();
      });
    this.getUTCDate = function() {
      return now.toUTCString();
    };
  }

  return function() {
    if (!_instance) {
      _instance = new init();
    }
    return _instance;
  };
})();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

上面的 timeTool 实际上是一个函数,_instance作为实例对象最开始赋值为 null,init 函数是其构造函数,用于实例化对象,立即执行函数返回的是匿名函数用于判断实例是否创建,只有当调用timeTool()时进行实例的实例化,这就是惰性单例的应用,不在 js 加载时就进行实例化创建, 而是在需要的时候再进行单例的创建。 如果再次调用, 那么返回的永远是第一次实例化后的实例对象

let instance1 = timeTool();
let instance2 = timeTool();
console.log(instance1 === instance2); //true
1
2
3

# 二、应用场景

# 2.1 命名空间

一个项目常常不只一个程序员进行开发和维护, 然后一个程序员很难去弄清楚另一个程序员暴露在的项目中的全局变量和方法。

如果将变量和方法都暴露在全局中, 变量冲突是在所难免的。就想下面的故事一样:

//开发者A写了一大段js代码
function addNumber() {}

//开发者B开始写js代码
var addNumber = "";

//A重新维护该js代码
addNumber(); //Uncaught TypeError: addNumber is not a function
1
2
3
4
5
6
7
8

命名空间就是用来解决全局变量冲突的问题,我们完全可以只暴露一个对象名,将变量作为该对象的属性,将方法作为该对象的方法,这样就能大大减少全局变量的个数。

//开发者A写了一大段js代码
let devA = {
  addNumber() {},
};

//开发者B开始写js代码
let devB = {
  add: "",
};

//A重新维护该js代码
devA.addNumber();
1
2
3
4
5
6
7
8
9
10
11
12

👆 上面代码中,devA 和 devB 就是两个命名空间,采用命名空间可以有效减少全局变量的数量,以此解决变量冲突的发生。

# 2.2 管理模块

上面说到的 timeTool 对象是一个只用来处理时间的工具库,但是实际开发过程中的库可能会有多种多样的功能,例如处理 ajax 请求,操作 dom 或者处理事件。

这个时候单例模式还可以用来管理代码库中的各个模块,例如下面的代码所示:

var devA = (function() {
  //ajax模块
  var ajax = {
    get: function(api, obj) {
      console.log("ajax get调用");
    },
    post: function(api, obj) {},
  };

  //dom模块
  var dom = {
    get: function() {},
    create: function() {},
  };

  //event模块
  var event = {
    add: function() {},
    remove: function() {},
  };

  return {
    ajax: ajax,
    dom: dom,
    event: event,
  };
})();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

上面的代码库中有 ajax,dom 和 event 三个模块,用同一个命名空间 devA 来管理。在进行相应操作的时候,只需要 devA.ajax.get()进行调用即可。这样可以让库的功能更加清晰。

# 三、 ES6 中的单例模式

# 3.1 ES6 创建对象

ES6 中创建对象时引入了 class 和 constructor 用来创建对象。

class Singleton {
  constructor(name) {
    this.name = name;
  }
}

let apple = new Singleton("苹果公司");
1
2
3
4
5
6
7

# 3.2 ES6 中创建单例模式

apple 应该是一个单例, 现在我们使用 ES6 的语法将constructor改写为单例模式的构造器。

class Singleton {
  constructor(name) {
    //首次使用构造器实例
    if (!Singleton.instance) {
      this.name = name;
      //将 this 挂载到 Singleton 这个类的instance属性上
      Singleton.instance = this;
    }
    return Singleton.instance;
  }
}
let apple = new Singleton("苹果公司");
let Alibaba = new Singleton("阿里巴巴");

console.log(apple === Alibaba); //true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 3.3 ES6 的静态方法优化代码

# ES6 的静态方法

类相当于实例的原型, 所有在类中定义的方法, 都会被实例继承。 如果在一个方法前, 加上 static 关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为 静态方法

class MyClass {
  constructor() {}
  set(key, value) {}
  get(key) {}
  static say(words) {
    alert(words);
  }
}

MyClass.say();
1
2
3
4
5
6
7
8
9
10

上面代码中,Foo 类的 classMethod 方法前有 static 关键字,表明该方法是一个静态方法,可以直接在 Foo 类上调用(Foo.classMethod()),而不是在 Foo 类的实例上调用。如果在实例上调用静态方法,会抛出一个错误,表示不存在该方法。

# 单例模式的优化

静态方法将不在实例化对象的方法中,因此里面不能有 this,使用的时候必须直接MyClass.say(),实例化对象不拥有这个方法。静态方法将被共享,因此所用内存减少

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

let apple = MyClass.getInstance("苹果公司");
let Alibaba = MyClass.getInstance("阿里巴巴");

console.log(apple === Alibaba); //true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
最后更新时间: 3/11/2021, 10:58:11 PM