接下来会有一个关于设计模式的专栏 有兴趣的可以插个眼👀
原文地址 www.patterns.dev/posts/singl...
预告: 下一篇代理模式
设计模式是软件开发的基本组成部分,因为它们为软件设计中经常出现的问题提供了典型的解决方案。设计模式不是提供特定的软件,而只是可用于以优化方式处理重复主题的概念。
在过去的几年里,Web开发生态系统发生了迅速的变化。虽然一些众所周知的设计模式可能根本不像以前那样有价值,但其他设计模式已经发展到使用最新技术解决现代问题。

Facebook的JavaScript库React在过去5年中获得了巨大的吸引力,与Angular,Vue,Ember和Svelte等竞争JavaScript库相比,目前是** NPM **上下载最频繁的框架。由于 React 的普及,设计模式已被修改、优化并创建新的模式,以便在当前的现代 Web 开发生态系统中提供价值。最新版本的 React 引入了一个名为 Hooks 的新功能,它在你的应用程序设计中起着非常重要的作用,可以取代许多传统的设计模式。
现代 Web 开发涉及许多不同类型的模式。本项目涵盖了使用 ES2015+ 的常见设计模式的实现、优势和陷阱、特定于 React 的设计模式及其使用 React Hooks 的可能修改和实现,以及更多有助于改进现代 Web 应用程序的模式和优化!
Singleton Pattern 单例模式
首先,让我们看一下使用 ES2015 类的单例会是什么样子。对于此示例,我们将构建一个 Counter
具有以下功能的类:
- 返回实例值
getInstance
的方法 - 返回
counter
变量当前值getCount
的方法 - 将
increment
的值递增 1counter
的方法 - 将
decrement
的值counter
递减 1 的方法
javascript
let counter = 0;
class Counter {
getInstance() {
return this;
}
getCount() {
return counter;
}
increment() {
return ++counter;
}
decrement() {
return --counter;
}
}
但是,此类不符合单例的标准!单例应该只能实例化一次。目前,我们可以创建 Counter
类的多个实例。
javascript
let counter = 0;
class Counter {
getInstance() {
return this;
}
getCount() {
return counter;
}
increment() {
return ++counter;
}
decrement() {
return --counter;
}
}
const counter1 = new Counter();
const counter2 = new Counter();
console.log(counter1.getInstance() === counter2.getInstance()); // false
通过调用 new
该方法两次,我们只是设置 counter1
并 counter2
等于不同的实例。 getInstance
该方法 counter1
counter2
返回的值有效地返回了对不同实例的引用:它们并不严格相等! 所以我们需要确保只能创建
Counter
类的一个实例。
确保只能创建一个实例的一种方法是创建一个名为 instance
的变量。在Counter
的构造函数中,我们可以在创建新实例时设置为 instance
对实例的 Counter
引用。我们可以通过检查 instance
变量是否已经有值来防止新的实例化。如果是这种情况,则实例已存在。这不应该发生:应该抛出错误以让用户知道
javascript
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
实例。但在此之前,我们还应该冻结实例。该方法 Object.freeze
确保使用代码无法修改单一实例。无法添加或修改冻结实例上的属性,这降低了意外覆盖单一实例上的值的风险。
javascript
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
和blueButton.js
模块redButton.js
:导入Counter
,并将Counter
的方法increment
作为事件侦听器添加到红色按钮,并通过调用getCount
该方法记录 的counter
当前值blueButton.js
:导入Counter
,并将Counter
的方法increment
作为事件侦听器添加到蓝色按钮,并通过调用getCount
该方法记录 的counter
当前值
arduino
// index.js
import "./redButton";
import "./blueButton";
console.log("Click on either of the buttons 🚀!");
javascript
// counter.js
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;
blueButton.js
和 redButton.js
从 counter.js
导入相同的Counter
实例

当我们在 blueButton.js
或者 redButton.js
中调用 increment
该方法时, Counter
实例上的 counter
属性值会在两个文件中更新并共享该值。无论我们单击红色还是蓝色按钮计数器不断递增 1 ,即使我们在不同的文件中调用该方法。
权衡利弊
将实例化限制为仅一个实例可能会节省大量内存空间。这样就不必每次都为新实例设置内存,而只需为该实例设置内存,该内存在整个应用程序中都会引用。然而,单例实际上被认为是一种反模式 (anti-pattern),或许可以在JavaScript中避免。
在许多编程语言中,例如Java或C++,不可能像在JavaScript中那样直接创建对象。在那些面向对象的编程语言中,我们需要创建一个类,由该类创建一个对象。创建的对象具有类实例的值,就像 JavaScript 示例中的值 instance
一样。
但是,上面示例中所示的类实现实际上是矫枉过正的。由于我们可以直接在 JavaScript 中创建对象,因此我们可以简单地使用常规对象来实现完全相同的结果。让我们介绍一下使用单例的一些缺点!
使用常规对象
让我们使用之前看到的相同示例。但是,这一次,counter
只是一个包含以下内容的对象:
- 一个
count
属性 - 方法
increment
将count
值递增 1 - 方法
decrement
将count
值递减 1
ini
let count = 0;
const counter = {
increment() {
return ++count;
},
decrement() {
return --count;
}
};
Object.freeze(counter);
export { counter };
由于对象是通过引用传递的,因此redButton.js
和 blueButton.js
都在导入counter
对象的引用。修改其中任一文件中的值 count
将修改 counter
的值,该值在两个文件中都可见。
测试
测试依赖于单例的代码可能会变得棘手。由于我们无法每次都创建新实例,因此所有测试都依赖于对上一个测试的全局实例的修改。在这种情况下,测试的顺序很重要,一个小的修改可能导致整个测试套件失败。测试后,我们需要重置整个实例,以便重置测试所做的修改。
Dependency hiding 依赖隐藏
在这种情况下,导入另一个模块时, superCounter.js
模块正在导入单例可能并不明显。在其他文件中,例如 index.js
在这种情况下,我们可能会导入该模块并调用其方法。这样,我们意外地修改了单例中的值。这可能会导致意外行为,因为可以在整个应用程序中共享 Singleton 的多个实例,这些实例也会被修改。
scss
// test.js
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);
});
javascript
// superCounter.js
import Counter from "./counter";
export default class SuperCounter {
constructor() {
this.count = 0;
}
increment() {
Counter.increment();
return (this.count += 100);
}
decrement() {
Counter.decrement();
return (this.count -= 100);
}
}
全局行为
单一实例应该能够在整个应用中得到引用。全局变量本质上表现出相同的行为:由于全局变量在全局范围内可用,因此我们可以在整个应用程序中访问这些变量。
具有全局变量通常被认为是一个糟糕的设计决策。全局范围污染最终可能会意外覆盖全局变量的值,这可能会导致许多意外行为。
在ES2015中,创建全局变量相当罕见。new let
and const
关键字通过保持使用这两个关键字声明的变量在块范围内来防止开发人员意外污染全局范围。JavaScript 中的新 module
系统通过能够从 export
模块中获取值以及其他 import
文件中的值,使创建全局可访问的值变得更加容易,而不会污染全局范围。
但是,单例的常见用例是在整个应用程序中具有某种全局状态。让代码库的多个部分依赖于同一个可变对象可能会导致意外行为。
通常,代码库的某些部分修改全局状态中的值,而其他部分则使用该数据。这里的执行顺序很重要:当(还没有)要消耗的数据时,我们不想先意外消耗数据!随着应用程序的增长,了解使用全局状态时的数据流可能会变得非常棘手,并且数十个组件相互依赖。
React 中的状态管理
在 React 中,我们经常通过Redux 或 React Context 等状态管理工具依赖全局状态,而不是使用 Singletons。尽管它们的全局状态行为可能看起来与单一实例的行为相似,但这些工具提供只读状态 ,而不是单一实例的可变 状态。使用 Redux 时,在组件通过dispatcher 发送action 后,只有纯函数reducers才能更新状态。
尽管使用这些工具不会神奇地消失全局状态的缺点,但我们至少可以确保全局状态按照我们预期的方式发生变异,因为组件不能直接更新状态。