单例模式详解:概念、实现与应用
引言
单例模式是面向对象编程中最简单但最常用的设计模式之一。它确保一个类只有一个实例,并提供一个全局访问点来访问该实例。虽然概念简单,但在实际应用中有许多细节需要注意。
什么是单例模式?
单例模式是一种创建型设计模式,它保证一个类只有一个实例,并提供一个全局访问点来访问这个实例。这个定义包含了几个关键点:
- 唯一实例:一个类只能创建一个实例
- 全局访问:提供一个全局访问点来获取这个实例
- 自行创建:类自己负责创建和管理唯一的实例
为什么需要单例模式?
在许多情况下,我们需要确保系统中只有一个实例存在:
1. 资源管理
某些资源非常稀缺或者昂贵,创建多个实例会造成资源浪费:
- 数据库连接池
- 线程池
- 日志文件句柄
2. 状态一致性
当需要在整个应用程序中维护一致的状态时:
- 全局配置对象
- 缓存管理器
- 应用状态管理器
3. 协调操作
当需要协调系统中的各种操作时:
- 打印任务队列
- 文件系统操作
- 设备驱动程序
单例模式的基本实现
让我们从最简单的单例模式实现开始:
javascript
// ES5 实现方式
var Singleton = (function() {
var instance;
function createInstance() {
var object = new Object("I am the instance");
return object;
}
return {
getInstance: function() {
if (!instance) {
instance = createInstance();
}
return instance;
}
};
})();
// 使用示例
var instance1 = Singleton.getInstance();
var instance2 = Singleton.getInstance();
console.log(instance1 === instance2); // true实现要点分析
- 私有实例变量:使用闭包创建私有变量
instance来存储唯一实例 - 延迟初始化:只有在第一次调用
getInstance时才创建实例 - 实例检查:每次调用都检查实例是否存在,不存在则创建,存在则返回
经典实现方式
1. 饿汉式(Eager Initialization)
饿汉式在类加载时就创建实例,这种方式简单但可能造成资源浪费:
javascript
class EagerSingleton {
// 在类定义时就创建实例
static instance = new EagerSingleton();
constructor() {
// 防止通过 new 直接创建实例
if (EagerSingleton.instance) {
return EagerSingleton.instance;
}
// 初始化操作
this.initialized = true;
EagerSingleton.instance = this;
}
static getInstance() {
return EagerSingleton.instance;
}
getInfo() {
return "Eager Singleton Instance";
}
}
// 使用示例
const instance1 = EagerSingleton.getInstance();
const instance2 = EagerSingleton.getInstance();
console.log(instance1 === instance2); // true2. 懒汉式(Lazy Initialization)
懒汉式在第一次使用时才创建实例,实现了延迟加载:
javascript
class LazySingleton {
constructor() {
if (LazySingleton.instance) {
return LazySingleton.instance;
}
// 初始化操作
this.createdAt = new Date();
LazySingleton.instance = this;
}
static getInstance() {
if (!LazySingleton.instance) {
LazySingleton.instance = new LazySingleton();
}
return LazySingleton.instance;
}
getCreationTime() {
return this.createdAt;
}
}
// 使用示例
console.log("程序启动");
// 延迟一段时间后才创建实例
setTimeout(() => {
const instance1 = LazySingleton.getInstance();
console.log("实例创建时间:", instance1.getCreationTime());
}, 2000);现代 JavaScript 实现方式
1. ES6 模块模式
利用 ES6 模块的特性,可以非常简洁地实现单例:
javascript
// logger.js
class Logger {
constructor() {
this.logs = [];
}
log(message) {
const timestamp = new Date().toISOString();
this.logs.push(`[${timestamp}] ${message}`);
console.log(`[${timestamp}] ${message}`);
}
getLogs() {
return [...this.logs]; // 返回副本,避免外部修改
}
}
// 导出单例实例
export default new Logger();javascript
// 使用示例
// app.js
import logger from './logger.js';
logger.log("应用启动");
// utils.js
import logger from './logger.js';
logger.log("执行工具函数");
// main.js
import logger from './logger.js';
import './app.js';
import './utils.js';
console.log("所有日志:", logger.getLogs());2. Symbol 实现私有构造函数
使用 Symbol 可以更好地实现私有构造函数:
javascript
const INSTANCE = Symbol('instance');
const PRIVATE = Symbol('private');
class ModernSingleton {
constructor(privateKey) {
// 检查是否通过 getInstance 方法调用
if (privateKey !== PRIVATE) {
throw new Error('请使用 ModernSingleton.getInstance() 获取实例');
}
this.data = "Modern Singleton Data";
}
static getInstance() {
if (!this[INSTANCE]) {
this[INSTANCE] = new ModernSingleton(PRIVATE);
}
return this[INSTANCE];
}
getData() {
return this.data;
}
setData(data) {
this.data = data;
}
}
// 使用示例
try {
const wrongInstance = new ModernSingleton(); // 抛出错误
} catch (e) {
console.log(e.message);
}
const instance1 = ModernSingleton.getInstance();
const instance2 = ModernSingleton.getInstance();
console.log(instance1 === instance2); // true单例模式的实际应用场景
1. 配置管理器实现
配置管理器是单例模式最典型的应用之一。在任何应用程序中,都需要一个全局可访问的配置系统来管理各种设置。
javascript
// ConfigManager.js
class ConfigManager {
constructor() {
// 防止重复实例化
if (ConfigManager.instance) {
return ConfigManager.instance;
}
// 默认配置
this.config = {
apiUrl: 'https://api.example.com',
timeout: 5000,
debug: false,
version: '1.0.0'
};
// 保存实例
ConfigManager.instance = this;
return this;
}
// 获取配置项
get(key) {
return this.config[key];
}
// 设置配置项
set(key, value) {
this.config[key] = value;
}
// 批量设置配置
setBulk(configObj) {
Object.assign(this.config, configObj);
}
// 获取所有配置
getAll() {
return { ...this.config };
}
}
// 导出单例实例
const configManager = new ConfigManager();
Object.freeze(configManager); // 冻结对象,防止意外修改
export default configManager;2. 日志记录器实现
日志记录器是另一个常见的单例模式应用场景。在整个应用程序中,我们通常希望所有的日志都通过统一的接口进行记录,以便于管理和分析。
javascript
// Logger.js
class Logger {
constructor() {
if (Logger.instance) {
return Logger.instance;
}
this.logs = [];
this.levels = ['DEBUG', 'INFO', 'WARN', 'ERROR'];
this.currentLevel = 'INFO';
Logger.instance = this;
return this;
}
// 设置日志级别
setLevel(level) {
if (this.levels.includes(level)) {
this.currentLevel = level;
}
}
// 检查日志级别是否应该被记录
shouldLog(level) {
return this.levels.indexOf(level) >= this.levels.indexOf(this.currentLevel);
}
// 格式化日志消息
formatMessage(level, message) {
const timestamp = new Date().toISOString();
return `[${timestamp}] ${level}: ${message}`;
}
// 记录日志
log(level, message) {
if (!this.shouldLog(level)) {
return;
}
const formattedMessage = this.formatMessage(level, message);
this.logs.push(formattedMessage);
// 在控制台输出
switch (level) {
case 'ERROR':
console.error(formattedMessage);
break;
case 'WARN':
console.warn(formattedMessage);
break;
case 'INFO':
console.info(formattedMessage);
break;
default:
console.log(formattedMessage);
}
}
// 便捷方法
debug(message) { this.log('DEBUG', message); }
info(message) { this.log('INFO', message); }
warn(message) { this.log('WARN', message); }
error(message) { this.log('ERROR', message); }
// 获取日志
getLogs() {
return [...this.logs];
}
}
// 导出单例实例
const logger = new Logger();
Object.freeze(logger);
export default logger;3. 缓存管理器实现
缓存管理器是单例模式的另一个重要应用场景。通过单例模式,我们可以确保整个应用程序共享同一个缓存实例,避免重复缓存相同的数据。
javascript
// CacheManager.js
class CacheManager {
constructor() {
if (CacheManager.instance) {
return CacheManager.instance;
}
this.cache = new Map();
this.timeouts = new Map();
CacheManager.instance = this;
return this;
}
// 设置缓存
set(key, value, ttl = 0) {
// 清除已存在的定时器
if (this.timeouts.has(key)) {
clearTimeout(this.timeouts.get(key));
this.timeouts.delete(key);
}
// 设置缓存值
this.cache.set(key, value);
// 如果设置了过期时间
if (ttl > 0) {
const timeout = setTimeout(() => {
this.cache.delete(key);
this.timeouts.delete(key);
}, ttl);
this.timeouts.set(key, timeout);
}
}
// 获取缓存
get(key) {
return this.cache.get(key);
}
// 检查缓存是否存在
has(key) {
return this.cache.has(key);
}
// 删除缓存
delete(key) {
if (this.timeouts.has(key)) {
clearTimeout(this.timeouts.get(key));
this.timeouts.delete(key);
}
return this.cache.delete(key);
}
// 清空所有缓存
clear() {
// 清除所有定时器
for (const timeout of this.timeouts.values()) {
clearTimeout(timeout);
}
this.timeouts.clear();
this.cache.clear();
}
// 获取缓存大小
size() {
return this.cache.size;
}
}
// 导出单例实例
const cacheManager = new CacheManager();
Object.freeze(cacheManager);
export default cacheManager;单例模式的优缺点
优点
- 内存节约:确保只创建一个实例,节省内存
- 全局访问:提供全局访问点,方便使用
- 延迟初始化:可以延迟创建实例,提高性能
- 实例控制:可以精确控制实例的数量
缺点
- 违反单一职责原则:既负责创建实例,又负责业务逻辑
- 难以测试:全局状态使得单元测试变得困难
- 隐藏依赖:使用者可能不清楚他们依赖于单例
- 多线程问题:在多线程环境中需要特殊处理(虽然 JavaScript 是单线程的)
总结
单例模式是最简单的设计模式之一,但在实际应用中需要注意其实现细节。在 JavaScript 环境中,由于其单线程特性和 ES6 模块系统,实现单例模式相对简单。
通过本章的学习,我们了解了:
- 单例模式的基本概念和用途
- 多种实现方式(饿汉式、懒汉式、模块模式等)
- 实际应用场景(配置管理器、日志记录器、缓存管理器)
- 单例模式的优缺点
在下一章中,我们将探讨单例模式的一些争议以及现代开发中的替代方案。