前言
在软件开发中,设计模式是解决特定问题的经典方案。今天我们要探讨的是单例模式(Singleton Pattern),这是一种创建型设计模式,也是JavaScript中最常用的设计模式之一。无论你是前端新手还是资深开发者,理解单例模式都能让你的代码更加优雅高效。
介绍单例模式
单例模式的核心思想是确保一个类只有一个实例,并提供一个全局访问点。就像公司里的CEO职位,无论多少人问"谁是CEO",得到的都是同一个对象。
举个栗子!
非单例模式:
js
//传统模式
class Dog {
constructor(name, breed, age) {
this.name = name;
this.breed = breed;
this.age = age;
}
}
const dog1 = new Dog('Buddy', 'Golden Retriever', 3);
const dog2 = new Dog('Luna', 'Husky', 2);
console.log(dog1 === dog2) //false
单例模式:
js
//单例模式
class Dog {
constructor(name, breed, age) {
if (Dog.instance) {
return Dog.instance;
}
this.name = name;
this.breed = breed;
this.age = age;
Dog.instance = this;
}
}
const dog1 = new Dog('Buddy', 'Golden Retriever', 3);
const dog2 = new Dog('Luna', 'Husky', 2);
console.log(dog1 === dog2); // true,两个变量引用同一个实例
为什么需要单例?
- 避免重复创建消耗资源的对象
- 确保全局状态的一致性
- 提供对唯一实例的受控访问
在JavaScript中,单例模式常用于:
- 全局状态管理(如Redux store)
- 缓存系统
- 日志记录器
- 浏览器中的window对象本身就是单例的典型例子
如何实现
下面我们通过一个本地存储封装的示例来演示单例模式的实现:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>设计模式之单例模式</title>
</head>
<body>
<script>
/*
* Storage 本地存储类
* 单例模式实现
* @func getItem 获取本地的值
* @func setItem 设置本地存储的值
*/
class Storage {
// 构造函数,可以指定命名空间防止键名冲突
constructor(namespace = 'storage') {
this.namespace = namespace;
}
// 静态方法获取单例实例
static getInstance(){
// 检查是否已存在实例
if (!Storage.instance) {
// 不存在则创建新实例
Storage.instance = new Storage();
}
// 返回已存在的实例
return Storage.instance;
}
// 获取本地存储的值
getItem(key) {
return localStorage.getItem(this.namespace + key);
}
// 设置本地存储的值
setItem(key, value) {
localStorage.setItem(this.namespace + key, value);
}
}
// 测试单例模式
const storage1 = Storage.getInstance();
const storage2 = Storage.getInstance();
// 验证两个变量是否指向同一个实例
console.log(storage1 === storage2, '!!!'); // 输出: true '!!!'
// 通过一个实例设置值
storage1.setItem('name','haha');
// 通过另一个实例获取值
console.log(storage1.getItem('name')); // 输出: "haha"
console.log(storage2.getItem('name')); // 输出: "haha"
</script>
</body>
</html>
代码解读:
-
我们创建了一个
Storage
类来封装localStorage操作 -
关键点在于
getInstance
静态方法:- 它检查类属性
Storage.instance
是否存在 - 不存在时才会创建新实例
- 确保无论调用多少次
getInstance()
都返回同一个实例
- 它检查类属性
-
测试代码验证了
storage1
和storage2
确实是同一个实例 -
并且通过一个实例修改的值,可以从另一个实例正确获取
用闭包实现单例
这里再提一种用闭包实现单例的方法,直接看代码
js
// 1. 基础构造函数
function StorageBase() {
// 空构造函数
}
// 2. 在原型上添加方法
StorageBase.prototype.getItem = function(key) {
return localStorage.getItem(key);
}
StorageBase.prototype.setItem = function(key, value) { // 注意:参数列表中缺少value
return localStorage.setItem(key, value); // 修正:原代码是settItem拼写错误
}
// 3. 单例封装(核心部分)
const Storage = (function() {
let instance = null; // 闭包保存单例实例
return function() { // 返回真正的构造函数
if(!instance) {
instance = new StorageBase(); // 首次调用时创建实例
}
return instance; // 总是返回同一个实例
}
})(); // 立即执行
// 4. 测试
const storage1 = Storage();
const storage2 = Storage();
console.log(storage1 === storage2) // true
这是一种es6之前,没有class语法糖的写法: 使用了闭包+立即执行函数的方式来实现单例模式,是一种经典的实现方式。下面我们逐部分解析:
-
闭包保存单例实例:
- 使用IIFE(立即执行函数)创建闭包环境
instance
变量被闭包保护,外部无法直接访问
-
惰性初始化(Lazy Initialization) :
- 只有第一次调用
Storage()
时才会创建实例 - 后续调用直接返回已创建的实例
- 只有第一次调用
-
原型方法:
- 将方法挂在
StorageBase.prototype
上 - 所有实例共享这些方法(虽然这里只有一个实例)
- 将方法挂在
与之前的class方式实现相比,两种方式的区别:
特性 | 类实现 | 闭包函数实现 |
---|---|---|
实例存储位置 | 类的静态属性 | 闭包变量 |
实现方式 | class语法 | 构造函数+原型链 |
访问方式 | 静态方法getInstance() | 直接函数调用 |
现代性 | ES6+ | ES5 |
私有性 | 较弱 | 闭包提供更好的封装 |
单例模式的重要性
单例模式在前端开发里可太重要啦,为啥这么说呢,因为它能解决下面这些问题:
- 资源优化:像数据库连接、WebSocket 连接这些老占资源的对象,用它就能避免反复创建
- 状态一致性:能保证像用户信息这种全局状态,在整个应用里都保持一致
- 内存管理:能减少那些没必要的内存占用,特别是在像移动端这种资源不咋够的环境里
- 访问控制:弄个统一的访问入口,以后要是想加日志、做验证啥的逻辑,都方便
实际应用场景:
- 全局状态管理库(如Redux、Vuex的store)
- 模态框/对话框管理
- 缓存系统
- 日志记录器
- 浏览器中的window/document对象
注意事项:
- 单例模式可能引入全局状态,过度使用会导致代码难以测试
- 在JavaScript中要考虑线程安全(虽然JS是单线程的)
- 考虑使用依赖注入来改善可测试性
通过合理使用单例模式,我们可以构建出更加健壮、高效的JavaScript应用。