单例模式,是设计模式中最简单且最常用的一种创建型模式。它的作用是确保一个类只有一个实例,并提供一个全局访问点。这种模式在资源管理、状态共享和性能优化中具有重要意义。
本文我将结合文档中的代码案例,从理论到实践带大家详细了解单例模式。
一、什么是单例模式?单例模式有什么用?
单例模式的定义:
单例模式(Singleton Pattern)是一种创建型设计模式,它能限制一个类仅能实例化一次,并通过提供一个全局访问点,让其他对象可以方便地获取该实例。
使用方式:
单例模式的使用方法有两种,分别是通过静态属性和闭包变量保存唯一的实例,并使用静态方法或函数封装来控制实例的创建逻辑。
下面,我将结合代码带大家对单例模式进行详细分析。
二、 通过 ES6 Class 实现单例(Storage 类)
先看代码:
class Storage {
static instance; // 静态属性,用于保存唯一实例
constructor() {
console.log(this, "实例化一次"); // 构造函数只会执行一次
}
static getInstance() { // 静态方法,提供全局访问点
if (!Storage.instance) {
Storage.instance = new Storage(); // 第一次调用时创建实例
}
return Storage.instance; // 后续调用直接返回已有实例
}
getItem(key) { // 封装 localStorage.getItem 方法
return localStorage.getItem(key);
}
setItem(key, value) { // 封装 localStorage.setItem 方法
localStorage.setItem(key, value);
}
}
// 使用方式
const storage1 = Storage.getInstance();
const storage2 = Storage.getInstance();
console.log(storage1 === storage2); // true,两者指向同一实例
上面这段代码,它定义了一个名为 Storage 的类,用于封装对 localStorage 的操作(getItem 和 setItem)。并且通过单例模式,确保它无论调用多少次 Storage.getInstance(),始终返回同一个实例。
代码分析:
-
静态属性
static instance:
静态属性属于类本身,而不是类的实例,通过Storage.instance,我们可以在不创建实例的情况下访问该属性。 -
静态方法
static getInstance():
静态方法可以直接通过类名来调用(如Storage.getInstance()),而无需实例化。它负责检查instance是否已存在,若不存在则创建新实例并保存到instance中。 -
构造函数
constructor():
构造函数仅在首次调用getInstance()时执行一次。后续调用会直接返回已存在的实例,避免重复初始化。
代码效果:
通过封装 localStorage 的操作,来实现单例的存储管理,这里无论调用多少次Storage.getInstance(),始终返回同一个实例。
三、 通过闭包实现单例(StorageBase 类)
代码解析
function StorageBase() {} // 基础类,定义公共方法
StorageBase.prototype.getItem = function(key) {
return localStorage.getItem(key);
};
StorageBase.prototype.setItem = function(key, value) {
localStorage.setItem(key, value);
};
// 闭包实现单例
const Storage = (function() {
let instance = null; // 闭包变量,保存唯一实例
return function() {
if (!instance) {
instance = new StorageBase(); // 第一次调用时创建实例
}
return instance; // 后续调用直接返回已有实例
};
})();
// 使用方式
const storage1 = new Storage();
const storage2 = new Storage();
console.log(storage1 === storage2); // true,两者指向同一实例
上面的代码通过闭包,实现了一个单例模式的 Storage 对象,它通过立即执行函数(IIFE)定义一个闭包变量 instance,确保 Storage 类的实例在整个程序运行期间仅创建一次。
代码分析:
- 闭包变量
instance:
这里通过立即执行函数(IIFE)创建的instance变量是一个自由变量,这个变量在外部无法直接访问或修改。 - 返回的函数:
IIFE 返回的函数是一个工厂函数,在每次调用时,它都会检查instance是否存在,若不存在,则创建新实例并赋值给instance。 - 原型链继承:
StorageBase类的getItem和setItem方法定义在原型链上,使所有的实例共享这些方法。
代码效果: 即使通过 new Storage() 创建实例,会返回同一个对象,闭包隔离了 instance,避免外部直接修改实例。
闭包实现单例 vs ES6 Class 实现单例
| 闭包实现(StorageBase) | ES6 Class 实现(Storage) | |
|---|---|---|
| 核心特性 | 通过闭包变量保存唯一实例,外部无法直接访问,安全性高。 | 通过类的静态属性保存实例,语法现代化,符合 ES6+ 标准。 |
| 使用方法 | 工厂函数返回实例(如 new Storage()),需手动控制闭包作用域。 | 静态方法 getInstance() 显式获取实例,避免直接使用 new Storage()。 |
| 适用场景 | 小型项目、工具类封装、对安全性要求高的场景(如敏感数据管理)。 | 现代项目、团队协作、需要继承和扩展功能的场景(如模块化开发)。 |
优缺点分析
闭包实现(StorageBase)
-
优点
- 封装性更强:闭包变量完全私有,外部无法修改或覆盖实例,安全性更高。
- 代码紧凑:通过闭包隔离逻辑,适合快速实现简单单例。
-
缺点
- 扩展性差:难以直接继承或组合其他类,需手动处理原型链。
- 可读性低:依赖闭包概念,初学者可能较难理解。
ES6 Class 实现(Storage)
-
优点
- 语法清晰:符合现代 JavaScript 开发习惯,代码结构更易读。
- 支持继承:天然支持
extends,便于功能扩展和模块化设计。 - 维护性高:类方法集中管理,适合长期维护和团队协作。
-
缺点
- 安全性较低:静态属性可能被外部直接访问或篡改(如
Storage.instance = null)。 - 需额外防护:需通过封装或私有属性(如
private static #instance)避免实例被破坏。
- 安全性较低:静态属性可能被外部直接访问或篡改(如
总结建议
- 闭包实现:适合对安全性要求高、代码量小的场景,例如工具类或敏感配置管理。
- ES6 Class 实现:更适合现代项目开发,尤其是需要团队协作、继承扩展或长期维护的场景。
四、总结
单例模式的核心价值
- 保证唯一性:通过静态属性或闭包变量限制实例数量。
- 全局访问:提供统一的访问入口,降低代码耦合度。
- 性能优化:减少重复创建对象的开销,提升系统效率。
注意事项
- 避免滥用:过度依赖单例可能导致代码难以测试和维护。
- 线程安全:在多线程环境中需额外处理并发问题(JavaScript 无需考虑)。
- 替代方案:在某些场景下,模块模式或依赖注入可能更合适。