单例模式是设计模式中的一种,其核心在于确保一个类在整个应用生命周期内只有一个实例,并提供唯一的全局访问点。这种模式在前端应用中的状态管理、缓存管理或数据库连接池等场景非常有用。以下将详细介绍在 JavaScript 中实现单例模式的六种常见方式。
一、对象字面量
这是创建单例最简单直接的方式,通过对象字面量快速定义一个对象。
const Singleton = {
property: 'value',
method() {
// 注意:方法内使用 this 在被解构调用时可能丢失上下文
// console.log(this.property);
// 更安全的做法是直接引用单例对象本身
console.log(Singleton.property);
}
};
// 使用
Singleton.method();
此方法无需实例化,定义即单例。唯一需要注意的是,如果将其方法解构出来单独调用,this指向会出问题,因此建议在方法内部直接通过对象名(Singleton)访问属性。
二、闭包与立即执行函数
利用 JavaScript 闭包的私有性,结合立即执行函数(IIFE)来封装实例变量。
const Singleton = (function() {
// 私有变量,用于存储唯一实例
let instance;
// 私有构造函数
function createInstance() {
const object = new Object('I am the instance');
return object;
}
// 对外暴露的公共 API
return {
getInstance: function() {
if (!instance) {
instance = createInstance();
}
return instance;
}
};
})();
// 使用
const instance1 = Singleton.getInstance();
const instance2 = Singleton.getInstance();
console.log(instance1 === instance2); // true
这种方式将创建实例的逻辑和实例本身完全封装在闭包内,外部只能通过getInstance方法获取唯一实例,实现了良好的封装。
三、ES6 Class 基础版
利用类的静态属性来存储唯一实例,并在构造函数中进行控制。
class Singleton {
constructor() {
// 如果已存在实例,则直接返回该实例
if (Singleton.instance) {
return Singleton.instance;
}
this.data = 'Singleton Data';
// 将当前创建的实例赋值给静态属性
Singleton.instance = this;
}
getData() {
return this.data;
}
setData(data) {
this.data = data;
}
}
// 使用
const s1 = new Singleton();
const s2 = new Singleton();
console.log(s1 === s2); // true
s1.setData('New Data');
console.log(s2.getData()); // ‘New Data’
这种实现直观,但构造函数并非完全私有,仍可通过new关键字调用,存在被多次实例化的风险(尽管后续调用会返回第一个实例)。
四、改进的 Class 实现 (TypeScript)
在 TypeScript 中,我们可以利用 private 构造器和静态方法,写出更健壮、更符合面向对象特性的单例。
class Singleton {
// 静态私有属性,存储唯一实例
private static instance: Singleton;
// 私有构造函数,防止外部使用 new 创建
private constructor() {}
// 公开的静态方法,用于获取唯一实例
public static getInstance(): Singleton {
if (!Singleton.instance) {
Singleton.instance = new Singleton();
}
return Singleton.instance;
}
// 其他实例方法...
}
// 使用
const s1 = Singleton.getInstance();
const s2 = Singleton.getInstance();
console.log(s1 === s2); // true
这是目前较为推荐的实现方式之一。私有构造函数彻底杜绝了从外部 new 的可能,获取实例的唯一途径就是调用静态方法 getInstance,逻辑清晰且类型安全。
五、ES6 模块模式的单例
利用 ES6 模块作用域内的变量是单例的特性,可以非常优雅地实现单例模式。这在管理数据库或缓存连接等场景中尤为常见。
// database.js 模块文件
let instance = null;
class Database {
constructor(config) {
if (instance) {
return instance;
}
this.connection = this.connect(config);
instance = this;
}
connect(config) {
// 模拟连接逻辑
return { connected: true, config };
}
}
// 导出一个工厂函数,用于获取唯一实例
export const getDatabaseInstance = (() => {
let instance = null;
return (config) => {
if (!instance) {
instance = new Database(config);
}
return instance;
};
})();
在这个例子中,模块内部的 instance 变量对于导入该模块的所有地方都是共享的,从而保证了 Database 类的唯一性。导出一个封装好的工厂函数,使用起来更方便。
六、ES6 模块即单例
这是最简单也最被低估的一种方式。在 ES6 模块系统中,一个模块如果导出一个已实例化的对象,那么这个对象在所有引入它的地方都是同一个实例。
// database.js
class Database {
constructor(config) {
this.connection = this.connect(config);
}
connect(config) {
return { connected: true, config };
}
}
// 直接导出唯一的实例
export default new Database({ host: 'localhost' });
// app.js
import dbInstance from ‘./database.js’;
// 无论被多少个其他模块引入,dbInstance 都是同一个对象
这种方式无需任何额外的单例控制代码,完全依赖模块系统本身的特性,非常适合在现代前端工程化应用中使用。
总结与选择建议
以上六种方法可归纳为四大类:对象字面量、闭包、ES6 Class 和 ES6 模块模式。
- 对象字面量:适合简单配置对象或工具集。
- 闭包:兼容性好,封装性强,适合传统脚本或库的开发。
- ES6 Class (TypeScript):代码结构清晰,类型安全,是当前推荐的主流方式。
- ES6 模块:最为简洁自然,是现代模块化开发中的首选方案,尤其在与 Vue 或 React 等框架结合时。
选择时,应根据项目环境(是否使用TypeScript、模块化规范)和具体场景(需要延迟初始化、线程安全等)来做出最适合的决策。