在前端开发中,设计模式是解决特定问题的最佳实践。本文将深入探讨单例模式(Singleton Pattern),它是最简单且实用的设计模式之一。
什么是单例模式?
单例模式的定义:确保一个类只有一个实例存在,那就要求它的构造方法一定不能是public公开的,即不能被外界实例化。那它的构造方法只能是private, 并且拥有一个当前类的静态成员变量,后面他要求向整个系统提供这个实例,即我们要再提供一个静态的方法,向外界提供当前类的实例,当前实例只能在内部进行实例化,不能够放到外面去。
为什么需要单例模式?
在实际开发中,某些对象我们只需要一个实例,例如:
浏览器中的
localStorage对象全局状态管理
配置管理器
数据库连接池
创建多个实例不仅浪费资源,还可能导致数据不一致的问题。
单例模式的实现方式
1. 对象字面量实现单例
这是最简单也是最常见的单例写法,直接通过一个对象字面量定义一个唯一的对象。
const Singleton = {
name: "Singleton",
doSomething() {
console.log("Doing something...");
}
};
// 使用
Singleton.doSomething(); // Doing something...
- 这种方式直接暴露了所有属性和方法,无法做到私有性。
- 适用于不需要复杂逻辑的场景。
2. 使用闭包封装私有变量
我们可以通过 IIFE(立即执行函数表达式)+ 闭包来创建一个带有私有状态的单例。
const Singleton = (function () {
let instance;
function init() {
// 私有变量
const privateVar = "I'm private";
return {
publicMethod() {
console.log(privateVar);
}
};
}
return {
getInstance() {
if (!instance) {
instance = init();
}
return instance;
}
};
})();
// 使用
const instance1 = Singleton.getInstance();
const instance2 = Singleton.getInstance();
console.log(instance1 === instance2); // true,同一个实例
instance1.publicMethod(); // I'm private
- 利用闭包实现了对
privateVar的封装。 - 通过
getInstance()控制唯一实例的创建。 - 第一次调用时创建实例,后续调用返回缓存的实例。
3. 使用 ES6 类 + 静态方法实现单例
ES6 引入了 class,虽然不能直接限制构造函数只被调用一次,但我们可以在类中模拟单例行为。
class Singleton {
constructor() {
if (Singleton.instance) {
return Singleton.instance;
}
this.data = "Singleton Data";
Singleton.instance = this;
}
getData() {
return this.data;
}
}
// 使用
const s1 = new Singleton();
const s2 = new Singleton();
console.log(s1 === s2); // true
console.log(s1.getData()); // Singleton Data
- 在构造函数中检查是否已经存在实例,若存在则返回已有实例。
- 利用了类的静态属性保存实例。
- 注意这种方式依赖于开发者自觉使用
new来获取实例。
如果你对于上述的代码实例还存在疑惑,让我们接下来看到下述代码片段加深学习
ES6 类实现单例模式
普通类的实例化问题
让我们先看一个普通的 Storage 类:
class Storage {
constructor() {
console.log(this, '~~~');
}
getItem(key) {
// 获取数据
}
setItem(key, value) {
// 设置数据
}
}
const storage1 = new Storage();
const storage2 = new Storage();
console.log(storage1 === storage2, '~~~'); // false
上面的代码每次使用 new 关键字都会创建一个新的实例,导致 storage1 和 storage2 是不同的对象。这在某些场景下可能会导致问题,比如多个实例操作同一个资源时的数据不一致。
使用静态方法实现单例模式
ES6 的 class 语法提供了 static 关键字,可以用来实现单例模式:
class Storage {
static instance;
constructor() {
console.log(this, '~~~');
}
// 静态方法
static getInstance() {
if (!Storage.instance) {
Storage.instance = new Storage();
}
return Storage.instance;
}
getItem(key) {
return localStorage.getItem(key);
}
setItem(key, value) {
return localStorage.setItem(key, value);
}
}
const storage1 = Storage.getInstance();
const storage2 = Storage.getInstance();
console.log(storage1 === storage2, '~~~'); // true
storage1.setItem('name', '卢老板');
console.log(storage1.getItem('name'), '~~~'); // 卢老板
console.log(storage2.getItem('name'), '~~~'); // 卢老板
这里的关键点是:
- 使用
static instance作为静态属性保存唯一实例 - 提供
static getInstance()方法检查实例是否存在:- 如果不存在,则创建新实例
- 如果存在,则返回已有实例
. 通过 getInstance() 方法而非直接使用 new 来获取实例
使用闭包实现单例模式
除了 ES6 的 class 语法,我们还可以使用函数闭包来实现单例模式:
function StorageBase() {
// 构造函数
}
StorageBase.prototype.getItem = function(key) {
return localStorage.getItem(key);
};
StorageBase.prototype.setItem = function(key, value) {
return 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
storage1.setItem('name', 'Mr Liu');
console.log(storage1.getItem('name'), '~~~'); // Mr Liu
console.log(storage2.getItem('name'), '~~~'); // Mr Liu
这种实现方式的关键点是:
- 使用立即执行函数(IIFE)创建闭包环境
- 在闭包中保存
instance变量,确保实例的唯一性 - 返回一个函数,该函数检查实例是否存在并返回
单例模式的优势
- 资源节约:只创建一个实例,减少内存占用
- 避免冲突:确保对共享资源的一致访问
- 全局访问点:提供统一的访问入口,方便管理
- 延迟初始化:实例在首次使用时才被创建(懒加载)
单例模式的实际应用
封装 localStorage 操作
正如我们的示例所示,单例模式非常适合封装浏览器的 localStorage API:
class StorageManager {
static instance;
static getInstance() {
if (!StorageManager.instance) {
StorageManager.instance = new StorageManager();
}
return StorageManager.instance;
}
getItem(key) {
return localStorage.getItem(key);
}
setItem(key, value) {
return localStorage.setItem(key, value);
}
removeItem(key) {
return localStorage.removeItem(key);
}
clear() {
return localStorage.clear();
}
}
// 使用
const storage = StorageManager.getInstance();
storage.setItem('user', JSON.stringify({name: '张三', age: 25}));
单例模式的注意事项
- 避免过度使用:不是所有对象都需要单例模式,过度使用会增加代码耦合度
- 测试难度:单例对象的状态在测试用例之间可能相互影响
- 并发问题:在多线程环境中需要考虑线程安全(JavaScript是单线程的,不存在这个问题)
总结
单例模式是一种简单但强大的设计模式,它确保一个类只有一个实例,并提供全局访问点。在JavaScript中,可以通过ES6的static关键字或闭包来实现单例模式。
单例模式适用于需要协调行为的场景,如管理共享资源、状态管理等。在实际开发中,我们应该根据具体需求选择是否使用单例模式,避免过度使用导致系统耦合度过高。