# 单例模式
TIP
在整个应用程序中共享单个全局实例
# 例子
首先,让我们看看使用 ES2015类的单例模式是什么样子的。对于这个示例,我们将构建一个 Counter 类,它具有:
- 返回实例值的 getInstance 方法
- 返回计数器变量当前值的 getCount 方法
- 将计数器的值加一的加法
- 把计数器的值减一的减法
let counter = 0;
class Counter {
getInstance() {
return this;
}
getCount() {
return counter;
}
increment() {
return ++counter;
}
decrement() {
return --counter;
}
}
但是,这个类不符合单例的标准!Singleton 应该只能被实例化一次。目前,我们可以创建 Counter 类的多个实例。
const counter1 = new Counter();
const counter2 = new Counter();
console.log(counter1.getInstance() === counter2.getInstance()); // false
通过两次调用这个新方法,我们只需要将 counter 1和 counter 2设置为等于不同的实例。GetInstance 方法返回的值对 Counter1和 Counter2有效地返回了对不同实例的引用: 它们不是严格相等的!
让我们确保只能创建 Counter 类的一个实例。
确保只能创建一个实例的一种方法是创建一个名为 instance 的变量。在 Counter 的构造函数中,我们可以在创建新实例时将 instance 设置为对该实例的引用。我们可以通过检查实例变量是否已经有值来防止新的实例化。如果是这种情况,那么已经存在一个实例。这种情况不应该发生: 应该抛出一个错误来让用户知道。
let instance;
let counter = 0;
class Counter {
constructor() {
if (instance) {
throw new Error("You can only create one instance!");
}
instance = this;
}
getInstance() {
return this;
}
getCount() {
return counter;
}
increment() {
return ++counter;
}
decrement() {
return --counter;
}
}
const counter1 = new Counter();
const counter2 = new Counter();
// Error: You can only create one instance!
太好了,我们不能再创建多个实例了。
让我们从 Counter.js 文件导出 Counter 实例。但是在这样做之前,我们还应该冻结该实例。急冻方法确保使用代码不能修改 Singleton。不能添加或修改冻结实例上的属性,这降低了意外覆盖 Singleton 上的值的风险。
let instance;
let counter = 0;
class Counter {
constructor() {
if (instance) {
throw new Error("You can only create one instance!");
}
instance = this;
}
getInstance() {
return this;
}
getCount() {
return counter;
}
increment() {
return ++counter;
}
decrement() {
return --counter;
}
}
const singletonCounter = Object.freeze(new Counter());
export default singletonCounter;
让我们看一下实现 Counter 示例的应用程序:
counter.js
: 包含 Counter 类,并将 Counter 实例作为默认导出导出
index.js
: 加载 redButton.js 和 bluButton.js 模块
redButton.js
: 导入 Counter,并将 Counter 的递增方法作为事件侦听器添加到红色按钮,并通过调用 getCount 方法记录 Counter 的当前值
blueButton.js
: 导入 Counter,并将 Counter 的递增方法作为事件侦听器添加到蓝色按钮,并通过调用 getCount 方法记录 Counter 的当前值
// redButton.js
import Counter from "./counter";
const button = document.getElementById("red");
button.addEventListener("click", () => {
Counter.increment();
console.log("Counter total: ", Counter.getCount());
});
// blueButton.js
import Counter from "./counter";
const button = document.getElementById("blue");
button.addEventListener("click", () => {
Counter.increment();
console.log("Counter total: ", Counter.getCount());
});
// index.js
import "./redButton";
import "./blueButton";
console.log("Click on either of the buttons 🚀!");
blueButton.js
和 redButton.js
都从 Counter.js
导入相同的实例,这个实例在两个文件中都作为 Counter 导入。
当我们在 redButton.js 或 bluButton.js 中调用递增方法时,Counter 实例上的 Counter 属性的值会在两个文件中都更新。单击红色或蓝色按钮并不重要: 所有实例共享相同的值。这就是为什么计数器一直递增一,即使我们在不同的文件中调用这个方法。
# 取舍
将实例化限制在一个实例中可能会节省大量内存空间。我们不必每次都为一个新实例设置内存,而只需为那个实例设置内存,这个实例在整个应用程序中都会被引用。然而,单例实际上被认为是反模式,并且可以(或者。.应该在 JavaScript 中避免)。
在许多编程语言中,如 Java 或 C++ ,不可能像在 JavaScript 中那样直接创建对象。在这些面向对象程序设计语言中,我们需要创建一个类来创建一个对象。创建的对象具有类的实例的值,就像 JavaScript 示例中的 instance 的值一样。
但是,上面示例中显示的类实现实际上有些夸张。因为我们可以直接在 JavaScript 中创建对象,所以我们可以简单地使用一个常规对象来实现完全相同的结果。让我们来谈谈使用单例模式的一些缺点吧!
# 使用一个普通对象
让我们使用前面看到的同一个例子,但是这一次,计数器只是一个对象包含:
- 一个 count 属性
- 将 count 的值加一的假发
- 把 count 的值减一的减法
let count = 0;
const counter = {
increment() {
return ++count;
},
decrement() {
return --count;
}
};
Object.freeze(counter);
export default counter;
由于对象是通过引用传递的,所以 redButton.js
和 bluButton.js
都导入了对同一个计数器对象的引用。修改这两个文件中的 count
的值将修改计数器上的值,该值在两个文件中都可见。
# 测试
测试依赖于 Singleton 的代码可能会很棘手。因为我们不能每次都创建新的实例,所有的测试都依赖于对前一个测试的全局实例的修改。在这种情况下,测试的顺序很重要,一个小小的修改就可能导致整个测试套件的失败。测试之后,我们需要重置整个实例,以便重置测试所做的修改。
import Counter from "../src/counterTest";
test("incrementing 1 time should be 1", () => {
Counter.increment();
expect(Counter.getCount()).toBe(1);
});
test("incrementing 3 extra times should be 4", () => {
Counter.increment();
Counter.increment();
Counter.increment();
expect(Counter.getCount()).toBe(4);
});
test("decrementing 1 times should be 3", () => {
Counter.decrement();
expect(Counter.getCount()).toBe(3);
});
# 全局行为
一个 Singleton 实例应该能够在整个应用程序中被引用。全局变量基本上显示相同的行为: 因为全局变量在全局范围内可用,所以我们可以在整个应用程序中访问这些变量。
拥有全局变量通常被认为是一个糟糕的设计决策。全局范围污染最终可能意外地覆盖全局变量的值,这可能导致许多意想不到的行为。
在 ES2015中,创建全局变量是相当罕见的。新的 let 和 const 关键字通过保持用这两个关键字块作用域声明的变量,防止开发人员意外地污染全局作用域。JavaScript 中的新模块系统使得创建全局可访问值变得更加容易,而不会污染全局范围,因为它能够从模块中导出值,并将这些值导入到其他文件中。
但是,Singleton 的常见用例是在整个应用程序中具有某种全局状态。代码库的多个部分依赖于同一个可变对象可能导致意外行为。
通常,代码库的某些部分修改全局状态中的值,而其他部分使用该数据。这里的执行顺序很重要: 我们不希望在没有数据可以使用的情况下意外地首先使用数据!在使用全局状态时理解数据流可能会随着应用程序的增长而变得非常棘手,因为许多组件相互依赖。
# 状态管理
在 React 中,我们通常通过状态管理工具(如 Redux 或 React Context)来依赖全局状态,而不是使用 Singleton。虽然它们的全局状态行为可能看起来类似于 Singleton,但是这些工具提供的是只读状态,而不是 Singleton 的可变状态。在使用 Redux 时,只有纯函数简化程序可以在组件通过调度程序发送操作之后更新状态。
虽然拥有全局状态的缺点不会因为使用这些工具而神奇地消失,但是我们至少可以确保全局状态按照我们想要的方式发生了变化,因为组件不能直接更新状态。