单例模式
1. 引言
单例模式(Singleton Pattern) 是一种创建型设计模式,它确保一个类只有一个实例,并提供一个全局访问点。本文将详细讲解单例模式在 JavaScript 中的多种实现方式,并配以实际应用的示例,帮助您深入理解并灵活运用于项目中。

2. 说明
问题
单例模式同时解决了两个问题, 所以违反了单一职责原则:
保证一个类只有一个实例。 为什么会有人想要控制一个类所拥有的实例数量? 最常见的原因是控制某些共享资源 (例如数据库或文件) 的访问权限。
它的运作方式是这样的: 如果你创建了一个对象, 同时过一会儿后你决定再创建一个新对象, 此时你会获得之前已创建的对象, 而不是一个新对象。
注意, 普通构造函数无法实现上述行为, 因为构造函数的设计决定了它必须总是返回一个新对象。

一个对象的全局访问节点
客户端甚至可能没有意识到它们一直都在使用同一个对象。
为该实例提供一个全局访问节点。 还记得你 (好吧, 其实是我自己) 用过的那些存储重要对象的全局变量吗? 它们在使用上十分方便, 但同时也非常不安全, 因为任何代码都有可能覆盖掉那些变量的内容, 从而引发程序崩溃。
和全局变量一样, 单例模式也允许在程序的任何地方访问特定对象。 但是它可以保护该实例不被其他代码覆盖。
还有一点: 你不会希望解决同一个问题的代码分散在程序各处的。 因此更好的方式是将其放在同一个类中, 特别是当其他代码已经依赖这个类时更应该如此。
如今, 单例模式已经变得非常流行, 以至于人们会将只解决上文描述中任意一个问题的东西称为单例。
解决方案
所有单例的实现都包含以下两个相同的步骤:
-
将默认构造函数设为私有, 防止其他对象使用单例类的 new 运算符。
-
新建一个静态构建方法作为构造函数。 该函数会 “偷偷” 调用私有构造函数来创建对象, 并将其保存在一个静态成员变量中。 此后所有对于该函数的调用都将返回这一缓存对象。
如果你的代码能够访问单例类, 那它就能调用单例类的静态方法。 无论何时调用该方法, 它总是会返回相同的对象。
真实世界类比
政府是单例模式的一个很好的示例。 一个国家只有一个官方政府。 不管组成政府的每个人的身份是什么, “某政府” 这一称谓总是鉴别那些掌权者的全局访问节点。
2.1 定义
单例模式的核心是确保一个类只有一个实例,并提供一个访问该实例的全局入口点。这对于需要在整个系统中共享状态或配置信息的场景十分有用。
2.2 特点
- 唯一性:整个应用程序中只能存在一个实例。
- 全局访问:提供一个全局访问点,所有模块都能方便地访问该实例。
- 可扩展性:可以根据需要延迟实例化,并在实例内部扩展功能。
2.3 实现方式
2.3.1 使用闭包和立即执行函数(IIFE)
const Singleton = (function () {
// 私有变量,存储单例实例
let instance;
// 私有方法,用于创建实例
function init() {
// 定义单例的属性和方法
return {
// 示例属性
_state: {},
// 获取状态
getState: function () {
return this._state;
},
// 设置状态
setState: function (key, value) {
this._state[key] = value;
},
};
}
// 公有方法,获取单例实例
return {
getInstance: function () {
if (!instance) {
instance = init();
}
return instance;
},
};
})();
// 测试代码
const instance1 = Singleton.getInstance();
const instance2 = Singleton.getInstance();
instance1.setState("name", "Singleton Instance");
console.log(instance2.getState()); // 输出: { name: 'Singleton Instance' }
console.log(instance1 === instance2); // 输出: true
2.3.2 使用类的静态属性
class Singleton {
constructor() {
if (Singleton.instance) {
// 如果实例已经存在,直接返回
return Singleton.instance;
}
// 初始化实例
this._state = {};
Singleton.instance = this; // 存储实例
}
// 获取状态
getState() {
return this._state;
}
// 设置状态
setState(key, value) {
this._state[key] = value;
}
}
// 测试代码
const instance1 = new Singleton();
const instance2 = new Singleton();
instance1.setState("language", "JavaScript");
console.log(instance2.getState()); // 输出: { language: 'JavaScript' }
console.log(instance1 === instance2); // 输出: true
扩展知识点 类的静态属性
在 JavaScript 中,类实际上是语法糖,本质上是基于原型的构造函数。类本身就是一个特殊的函数,因此可以像使用对象一样,动态地向类添加属性和方法。
当您在类中使用 ConfigManager.instance 时,实际上是在向类本身(构造函数)添加一个静态属性 instance。
class ConfigManager {
constructor() {
// 检查类的静态属性 instance 是否已存在
if (ConfigManager.instance) {
return ConfigManager.instance;
}
// 初始化配置
this._config = {
apiBaseUrl: "https://api.example.com",
timeout: 5000,
retryAttempts: 3,
};
// 将当前实例赋值给类的静态属性 instance
ConfigManager.instance = this;
}
// 获取配置
getConfig(key) {
return this._config[key];
}
// 设置配置
setConfig(key, value) {
this._config[key] = value;
}
// 获取所有配置
getAllConfigs() {
return this._config;
}
}
在上述代码中,虽然没有显式声明 ConfigManager.instance,但在 JavaScript 中,可以直接在类(构造函数)上添加或访问属性。也就是说,ConfigManager 作为一个函数对象,可以动态添加属性 instance。
2.3 适用场景
- 配置管理器:需要在全局范围内共享和管理配置信息。
- 日志记录器:全局统一的日志记录机制,方便调试和监控。
- 缓存系统:在内存中缓存数据,提高性能。
- 发布订阅:通过单例模式实现发布订阅。
- 数据库连接池:管理数据库连接,避免重复创建连接。
3. 单例模式的实际应用
3.1 配置管理器
class ConfigManager {
constructor() {
if (ConfigManager.instance) {
return ConfigManager.instance;
}
this._config = {
apiBaseUrl: "https://api.example.com",
timeout: 5000,
retryAttempts: 3,
};
ConfigManager.instance = this;
}
// 获取配置
getConfig(key) {
return this._config[key];
}
// 设置配置
setConfig(key, value) {
this._config[key] = value;
}
// 获取所有配置
getAllConfigs() {
return this._config;
}
}
// 测试代码
const configManager1 = new ConfigManager();
const configManager2 = new ConfigManager();
console.log(configManager1 === configManager2); // 输出: true
// 修改配置
configManager1.setConfig("timeout", 10000);
// 获取配置
console.log(configManager2.getConfig("timeout")); // 输出: 10000
3.2 发布订阅
发布订阅模式和单例模式是两个独立的设计模式,它们各自解决不同的问题
- 发布订阅模式:用于处理对象之间的一对多关系,实现松耦合的事件通信机制
- 单例模式:确保一个类只有一个实例,并提供一个全局访问点
但在实际应用中,发布订阅系统可以使用单例模式来实现
- 全局唯一性:确保所有发布者和订阅者使用同一个事件总线
- 状态管理:统一管理订阅关系和事件分发
- 资源效率:避免创建多个事件处理实例
class PubSub {
// 私有实例
static #instance = null;
// 存储所有事件
#events = {};
// 私有构造函数
constructor() {
if (PubSub.#instance) {
return PubSub.#instance;
}
PubSub.#instance = this;
}
// 获取单例实例
static getInstance() {
if (!PubSub.#instance) {
PubSub.#instance = new PubSub();
}
return PubSub.#instance;
}
// 订阅事件
subscribe(eventName, callback) {
if (!this.#events[eventName]) {
this.#events[eventName] = [];
}
this.#events[eventName].push(callback);
}
// 发布事件
publish(eventName, data) {
if (!this.#events[eventName]) {
return;
}
this.#events[eventName].forEach((callback) => callback(data));
}
// 取消订阅
unsubscribe(eventName, callback) {
if (!this.#events[eventName]) {
return;
}
this.#events[eventName] = this.#events[eventName].filter(
(cb) => cb !== callback
);
}
}
// 测试代码
// 获取PubSub实例
const pubsub1 = PubSub.getInstance();
const pubsub2 = PubSub.getInstance();
// pubsub1 和 pubsub2 是同一个实例
console.log(pubsub1 === pubsub2); // true
// 订阅消息
pubsub1.subscribe("userLogin", (data) => {
console.log("用户登录:", data);
});
// 发布消息
pubsub2.publish("userLogin", { userId: 123, username: "adny" });
3.3 创建数据库连接
const { MongoClient } = require("mongodb");
class Database {
// 静态属性,用于存储唯一实例
static instance = null;
constructor() {
if (Database.instance) {
// 如果实例已存在,直接返回
return Database.instance;
}
// 初始化数据库配置
this._config = {
url: "mongodb://localhost:27017",
dbName: "myDatabase",
};
this._client = null; // MongoClient 实例
this._db = null; // 数据库实例
Database.instance = this; // 缓存当前实例
}
// 连接到数据库
async connect() {
if (!this._client) {
try {
this._client = await MongoClient.connect(this._config.url, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
console.log("数据库连接成功");
this._db = this._client.db(this._config.dbName);
} catch (error) {
console.error("数据库连接失败", error);
throw error;
}
}
return this._db;
}
// 获取数据库实例
getDB() {
if (!this._db) {
throw new Error("尚未连接到数据库,请先调用 connect() 方法");
}
return this._db;
}
// 关闭数据库连接
async close() {
if (this._client) {
await this._client.close();
this._client = null;
this._db = null;
console.log("数据库连接已关闭");
}
}
}
// 测试代码
// 创建两个实例
const dbInstance1 = new Database();
const dbInstance2 = new Database();
console.log("两个实例是否相同:", dbInstance1 === dbInstance2); // 输出: true
// 连接到数据库
await dbInstance1.connect();
// 获取数据库实例
const db1 = dbInstance1.getDB();
const db2 = dbInstance2.getDB();
console.log("两个数据库实例是否相同:", db1 === db2); // 输出: true
// 进行数据库操作,例如插入数据
const collection = db1.collection("users");
const result = await collection.insertOne({ name: "张三", age: 28 });
console.log("插入结果:", result.insertedId);
// 关闭数据库连接
await dbInstance1.close();