JavaScript 中如何优雅地实现单例?多种方案对比解析
单例模式(Singleton Pattern) 是一种常用的创建型设计模式,在 JavaScript 中,由于其动态语言特性和灵活的对象模型,实现单例的方式也多种多样。
一、 什么是单例模式?
单例模式的核心思想是:保证一个类只有一个实例,并提供一个全局访问点来获取这个实例。
这种模式常用于管理全局状态 、配置对象 、日志记录器 、数据库连接等需要唯一实例的场景。这是因为这些对象创建和销毁的成本比较高,使用单例可以避免重复创建,减少开销。
二、JavaScript 中的单例实现方式
-
使用对象字面量(Object Literal)
这是最简单、最直接的一种方式,适用于不需要延迟加载的场景。
jsconst Singleton = { value: 42, getValue() { return this.value; } }; console.log(Singleton.getValue()); // 输出 42
这种方式的代码简洁,易于理解和维护,但不具备延迟初始化能力,不能动态创建实例。
-
使用闭包 + 工厂函数 + IIFE
通过闭包封装私有变量,实现真正的惰性加载(Lazy Initialization)。
js// 构造函数 function StorageBase() {} StorageBase.prototype.get = function (key) { return localStorage.getItem(key); }; StorageBase.prototype.set = function (key, value) { localStorage.setItem(key, value); }; // 闭包+立即执行函数 Storage是闭包实际上是闭包函数,这个闭包函数中引用了instance这个自由变量 const Storage = (function () { // instance 用于指向StorageBase的实例对象 let instance = null; return function () { if (!instance) { instance = new StorageBase(); } return instance; }; })(); const storage = new Storage(); const storage2 = new Storage(); console.log(storage === storage2); // true
代码解析:
- 在上述这段代码中
StorageBase
是一个普通的构造函数,提供get与set,底层使用的是localStorage
来实现存储。 - Storage是一个立即执行函数返回的函数 ,也就是闭包函数。在这里利用闭包维护一个私有变量instance,这个instance用于存储
StorageBase
的实例化对象。当Storage第一次执行时,会创建StorageBase
的实例化对象并存储到instance这个自由变量中,之后每次执行Storage这个构造函数都会返回这个实例化对象。 - 使用new关键字创建一个实例对象时,若构造函数返回的是一个对象或函数,那么new表达式将不会再返回之前构造函数创建的新对象而是直接返回这个指定的对象。上述构造函数Storage返回的便是一个对象,所以会有上述的结果。
使用闭包 + 工厂函数 + IIFE的优点是实现了惰性加载,保证线程安全(在 JavaScript 单线程环境中),但这种方式略复杂,需要理解闭包和立即执行函数。
- 在上述这段代码中
-
使用 ES6 类 + 静态方法
JavaScript 是一门基于原型的语言,早期并没有
class
的概念,而是通过函数和原型链来实现面向对象编程。ES6 引入了class
语法糖,使得代码更易读、结构更清晰,但其底层依然是基于原型的。jsclass Storage { // 静态方法用于获取实例 static getInstance() { // 静态方法中的 this 指向类本身 if (!this.instance) { this.instance = new Storage(); } return this.instance; } // 构造函数 constructor() { // 构造函数中的 this 指向实例对象 this.storage = window.localStorage; } } const storage1 = Storage.getInstance(); const storage2 = Storage.getInstance(); console.log(storage1 === storage2); // true
在面向对象中
public
和private
等关键字都是属性实例对象上的,而static是属于类的。静态方法只能访问静态属性和静态方法,实例方法既可以访问实例属性、方法,也可以访问静态属性、方法。代码解析:
-
getInstance()
是一个静态方法,调用时this
指向的是类本身。 -
第一次调用时,
this.instance
不存在,这时会在类上动态添加一个静态属性instance 。之后创建Storage的实例(走第三步)并赋值给它。后续调用会直接返回this.instance
,从而保证单例。 -
constructor是一个构造方法,调用时this指向的是实例对象 ,
this.storage = window.localStorage
实际上是在实例对象上面动态添加了storage属性这个属性值,这个属性值是window.localStorage
使用这种方式如果直接使用
new Singleton()
,可能会绕过单例逻辑,需要始终使用getInstance()
方法。 -
-
使用 ES6 模块导出(Module Pattern)
在 ES6 模块系统中,模块本身是天然的单例,非常适合用来实现全局共享对象。
js
// singleton.js
export default {
value: 42,
getValue() {
return this.value;
}
};
其他模块中使用:
js
import singleton from './singleton.js';
console.log(singleton.getValue()); // 输出 42
优点 :简洁、天然单例,适合现代前端项目。 缺点:不支持惰性加载,除非手动封装。
-
使用 Proxy 实现更灵活控制(进阶)
通过
Proxy
拦截构造函数调用,可以更灵活地控制实例的创建过程。jslet instance = null; const SingletonProxy = new Proxy({}, { construct() { if (!instance) { instance = { value: 42 }; } return instance; } }); const a = new SingletonProxy(); const b = new SingletonProxy(); console.log(a === b); // true
优点 :灵活,可以拦截和控制对象创建行为。 缺点:理解成本高,适用于进阶用途。
三、各种 实现方式对比表
实现方式 | 是否惰性加载 | 是否推荐 | 适用场景 |
---|---|---|---|
对象字面量 | ❌ | ✅ | 快速定义常量对象 |
闭包封装 | ✅ | ✅ | 惰性加载、传统写法 |
类 + 静态方法 | ✅ | ✅ | 面向对象风格、逻辑封装 |
模块导出 | ❌ | ✅ | ES6+ 模块系统、共享配置对象 |
Proxy | ✅ | ⚠️ | 高级控制、特殊需求 |