单例模式
约 1268 字大约 4 分钟
2025-04-30
单例模式是一种创建型设计模式,其核心目标是确保一个类在系统中只有一个实例,并提供一个全局访问点来获取该实例。它通过控制实例化过程,避免重复创建对象,常用于管理全局共享资源。
一、核心特点
- 唯一性:整个系统中只存在一个类的实例。
- 全局访问:通过静态方法或全局变量提供统一的访问入口。
- 推迟初始化:实例在第一次被访问时才创建,节省资源。
二、实现关键点
- 私有化构造函数:防止外部通过
new
创建实例 - 静态成员保存实例:通过类自身的静态属性存储唯一实例
- 全局访问方法:如
getInstance()
控制实例的创建和访问
三、典型应用场景
- 数据库连接池(避免重复建立连接
MongoDB
) - 日志记录器(全局共享日志对象)
- 配置管理类(统一读取配置文件)
- 缓存系统(如全局状态管理
Vuex
、Pinia
) - 硬件资源访问(如打印机服务)
四、优缺点对比
1、优点
- 节省资源和操作时间:避免重复创建对象,节省内存。
- 全局访问:提供统一的访问入口,简化代码。
- 数据共享:全局状态管理,方便数据共享。
2、缺点
- 违反单一职责原则:同时管理实例化和业务逻辑。
- 可能隐藏代码耦合:全局状态管理可能隐藏代码耦合关系。
- 多线程环境下需额外处理:多线程环境下需额外处理同步问题。
五、实现方案
在 JavaScript 中实现单例模式有多种方式,以下是 5 种主流实现方法及其适用场景:
方法 1:对象字面量(最简单直接)
- 无需类机制,直接通过对象管理
- 适合简单场景,但无法实现私有变量
// 直接创建单例对象
const AppConfig = {
instance: null,
init(config) {
if (!this.instance) {
this.instance = {
...config,
log() {
console.log("Config loaded");
},
};
}
return this.instance;
},
};
// 使用
const config1 = AppConfig.init({ theme: "dark" });
const config2 = AppConfig.init(); // 返回同一个实例
console.log(config1 === config2); // true
方法 2:闭包 + IIFE(推荐经典方式)
- 利用闭包保护私有变量
instance
- 支持延迟初始化,灵活控制实例创建
const Singleton = (() => {
let instance; // 闭包保存私有实例
function createInstance(config) {
return {
config,
log() {
console.log("Instance ID:", this.config.id);
},
};
}
return {
getInstance(config) {
if (!instance) {
instance = createInstance(config || { id: Date.now() });
}
return instance;
},
};
})();
// 使用
const a = Singleton.getInstance({ id: 100 });
const b = Singleton.getInstance();
a.log(); // Instance ID: 100
console.log(a === b); // true
方法 3:ES6 Class(现代写法)
- 使用类的静态私有字段存储实例
- 通过构造函数拦截实现单例
- 需注意:直接调用
new
仍可能被绕过(可通过抛出错误防御)
class DatabaseConnection {
static #instance; // 私有静态字段(ES2022+)
constructor() {
if (DatabaseConnection.#instance) {
return DatabaseConnection.#instance;
}
// 初始化连接逻辑
this.status = "connected";
DatabaseConnection.#instance = this;
}
query(sql) {
console.log(`Executing: ${sql}`);
}
}
// 测试
const db1 = new DatabaseConnection();
const db2 = new DatabaseConnection();
console.log(db1 === db2); // true
db1.query("SELECT * FROM users"); // Executing: SELECT * FROM users
方法 4:模块化(Node.js 天然单例)
- Node.js 模块系统天然支持单例(模块缓存机制)
- 适合后端服务中的全局资源管理
// logger.js(Node.js 模块)
let instance = null;
function createLogger() {
return {
logs: [],
log(message) {
this.logs.push(message);
console.log(`[LOG] ${message}`);
},
};
}
// 导出单例
module.exports = () => {
if (!instance) {
instance = createLogger();
}
return instance;
};
// 使用
const getLogger = require("./logger.js");
const logger1 = getLogger();
const logger2 = getLogger();
console.log(logger1 === logger2); // true
方法 5:Proxy 代理(高级玩法)
- 通过代理拦截
new
操作 - 无需修改原类代码,实现单例包装
function createSingleton(Class) {
let instance;
return new Proxy(Class, {
construct(target, args) {
if (!instance) {
instance = new target(...args);
}
return instance;
},
});
}
// 定义普通类
class Logger {
constructor() {
this.logs = [];
}
log(msg) {
console.log(msg);
}
}
// 创建单例版本
const SingletonLogger = createSingleton(Logger);
// 测试
const l1 = new SingletonLogger();
const l2 = new SingletonLogger();
console.log(l1 === l2); // true
六、关键注意事项
1、防止反射攻击
class SecureSingleton {
constructor() {
if (SecureSingleton.instance) {
throw new Error("Use getInstance() instead");
}
SecureSingleton.instance = this;
}
static getInstance() {
return SecureSingleton.instance || new SecureSingleton();
}
}
2、多线程问题
JavaScript 是单线程语言,通常无需考虑线程安全问题。但在 Web Workers 等特殊场景下需使用 Atomics
或锁机制。
3、序列化/反序列化
如果单例需要持久化,需实现 toJSON()
和恢复逻辑,避免破坏单例特性。
七、各方案对比
方案 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
对象字面量 | 简单直接 | 无法实现私有变量 | 小型工具类 |
闭包 + IIFE | 灵活可控,支持私有变量 | 代码结构稍复杂 | 通用场景 |
ES6 Class | 现代语法,直观易读 | 需处理 new 绕过问题 | 面向对象项目 |
模块化 | Node.js 原生支持 | 仅限模块系统环境 | 后端服务 |
Proxy 代理 | 无侵入式改造 | 兼容性要求(ES6+) | 需要包装已有类的场景 |
选择方案时根据项目需求(浏览器兼容性、代码规范、是否需要私有变量等)灵活选择。
更新日志
2025/8/24 08:17
查看所有更新日志
e7112
-1于