文章

单例模式

单例模式

单例模式

单例模式

概念

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

单例模式是创建型设计模式的一种。针对全局仅需一个对象的场景,如线全局缓存、window 对象等。

在 JavaScript 开发中,单例模式的用途同样非常广泛。试想一下,当我们单击登录按钮的时候,页面中会出现一个登录浮窗,而这个登录浮窗是唯一的,无论单击多少 次登录按钮,这个浮窗都只会被创建一次,那么这个登录浮窗就适合用单例模式来创建。

单例模式要求一个 “唯一” 和 “全局访问” 的对象,在 JavaScript 中类似全局对象,刚好满足单例模式的两个特点:“唯一” 和 “全局访问”。虽然它不是正规的单例模式,但不可否认确实具备类单例模式的特点。

模式实现

使用一个变量存储类实例对象(值初始为 **null/undefined** )。进行类实例化时,判断类实例对象是否存在,存在则返回该实例,不存在则创建类实例后返回。多次调用类生成实例方法,返回同一个实例对象。

实例

全局变量实现单例

1
2
3
4
5
6
7
8
9
10
11
let instance = null;
let getInstance = function (arg) {
  if (!instance) {
    instance = arg;
  }
  return instance;
}

let a = getInstance('a');
let b = getInstance('b');
console.log(a === b); // true

虽然这种全局变量可以实现单例,但因其自身的问题,不建议在实际项目中将其作为单例模式的应用,特别是中大型项目的应用中,全局变量的维护成本高。

构造函数实现单例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Singleton(name) {
  this.name = name;
  this.instance = null;
}

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

let a = Singleton.getInstance('a');
let b = Singleton.getInstance('b');
console.log(a===b); // true

这种方式也有缺点,就是我们必须调用 getInstance 来创建对象,一般我们创建对象都是利用new操作符。

透明的单例模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let Singleton = (function () {
  let instance;
  return function (name) {
    if (instance) {
      return instance;
    }
    this.name = name;
    return instance = this;
  }
})()

let a = new Singleton('a');
let b = new Singleton('b');
console.log(a === b); // true

这种方法也有缺点:不符合单一职责原则,这个对象其实负责了两个功能:单例操作和创建对象。

单一职责原则:一个程序或一个类或一个方法只做好一件事,如果功能过于复杂,我们就拆分开,每个方法保持独立,减少耦合度。

“代理版”单例模式

通过“代理”的形式,意图解决:将管理单例操作,与对象创建操作进行拆分,实现更小的粒度划分,符合“单一职责原则”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function People(name) {
  this.name = name;
}

let Singleton = (function () {
  let instance;
  return function (name) {
    if (instance) {
      return instance
    }
    return instance = new People(name);
  }
})()

let a = new Singleton('a')
let b = new Singleton('b')
console.log(a === b); // true

这种方法也有缺点:代码不能复用。如果我们有另外一个对象也要利用单例模式,那我们不得不写重复的代码

通用的单例模式

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
28
29
function Singleton(Obj) {
  let instance;
  return function () {
    if (instance) {
      return instance;
    }
    return instance = new Obj(arguments);
  }
}

function People(name) {
  this.name = name;
}

function Animal(name) {
  this.name = name;
}

let peopleSingleton = Singleton(People);
let animalSingleton = Singleton(Animal);

let a = new peopleSingleton('a');
let b = new peopleSingleton('b');
console.log(a === b);


let A = new animalSingleton('A');
let B = new animalSingleton('B');
console.log(A === B);

这种方法就可以复用单例 Singleton 代码。

ES6单例模式

1
2
3
4
5
6
7
8
9
10
11
12
13
class People {
  constructor(name) {
    if (!People.instance) {
      this.name = name;
      People.instance = this;
    }
    return People.instance;
  }
}

const a = new People('a');
const b = new People('b');
console.log(a === b); // true

开发案例

实现一个点击登录按钮弹出登录框的例子

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
28
29
30
31
32
33
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
  <style>
    .dialog {
      width: 200px;
      height: 200px;
      text-align: center;
      line-height: 200px;
      border: 1px solid red;
    }
  </style>
</head>
<body>
<button id="loginBtn">登录</button>
</body>
<script>
  let createLoginLayer = function () {
    let div = document.createElement('div');
    div.className = 'dialog';
    div.innerHTML = '我是登录浮窗';
    div.style.display = 'none';
    document.body.appendChild(div);
    return div;
  };
  document.getElementById('loginBtn').onclick = function () {
    let loginLayer = createLoginLayer();
    loginLayer.style.display = 'flex';
  };
</script>
</html>

这种方式每次点击按钮都会创建一个登录框

改进

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
  <style>
    .dialog {
      width: 200px;
      height: 200px;
      text-align: center;
      line-height: 200px;
      border: 1px solid red;
    }
  </style>
</head>
<body>
<button id="loginBtn">登录</button>
<button id="registerBtn">注册</button>
</body>
<script>
  // 单例
  function getSingle(fn) {
    let result;
    return function () {
      if (!result) {
        result = fn.apply(this, arguments)
      }
      return result;
    }
  }

  // 创建登录弹窗
  function createLoginDialog(text) {
    let div = document.createElement('div');
    div.innerHTML = text;
    div.className = 'dialog';
    div.style.display = 'none';
    document.body.appendChild(div);
    return div;
  }

  // 创建注册弹窗
  function createRegisterDialog(text) {
    let div = document.createElement('div');
    div.innerHTML = text;
    div.className = 'dialog';
    div.style.display = 'none';
    document.body.appendChild(div);
    return div;
  }

  let createSingleLoginDialog = getSingle(createLoginDialog);
  let createSingleRegisterDialog = getSingle(createRegisterDialog);

  document.getElementById('loginBtn').onclick = function () {
    let loginLayer = createSingleLoginDialog('我是登录弹窗');
    loginLayer.style.display = 'block';
  };

  document.getElementById('registerBtn').onclick = function () {
    let loginLayer = createSingleRegisterDialog('我是注册弹窗');
    loginLayer.style.display = 'block';
  };
</script>
</html>

登录、注册复用了一套 单例 代码。

适用场景

“单例模式的特点,意图解决:维护一个全局实例对象。”

  1. 引用第三方库(多次引用只会使用一个库引用,如 jQuery)
  2. 弹窗(登录框,信息提升框)
  3. 购物车 (一个用户只有一个购物车)
  4. 全局态管理 store (Vuex / Redux)

项目中引入第三方库时,重复多次加载库文件时,全局只会实例化一个库对象,如 jQuery,moment …, 其实它们的实现理念也是单例模式应用的一种:

1
2
3
4
5
6
// 引入代码库 libs(库别名)
if (window.libs != null) {
  return window.libs;    // 直接返回
} else {
  window.libs = '...';   // 初始化
}
本文由作者按照 CC BY 4.0 进行授权

© 独行的风. 保留部分权利。

本站采用 Jekyll 主题 Chirpy

本站总访问量 本站访客数 本文阅读量