单例的关键在于手动控制实例创建逻辑,而非class语法本身。通过static getInstance()方法配合静态属性缓存实例,确保多次调用返回同一引用;或直接在ES模块顶层导出单一实例,利用模块缓存机制天然实现单例。

用 JS 的 class 语法实现单例,关键不是靠 class 本身“锁住”实例,而是**手动控制构造逻辑,确保无论调用多少次,返回的始终是同一个对象引用**。ES6 class 没有私有构造器,new MyClass() 每次都会新建实例——所以必须绕过直接 new,改用静态方法统一管控。
用 static getInstance() 封装实例创建
这是最清晰、最推荐的方式。把实例缓存在类的静态属性上,首次调用时初始化,后续直接返回缓存值。
- 用
static instance = null存储唯一实例(ES2022+ 支持公有静态字段;旧环境可用MyClass.instance = null在类外声明) -
getInstance()内部判断instance是否已存在,不存在则new并赋值 - 构造函数不做限制(JS 无法真正禁止
new),但可通过注释或抛错提示“请使用 getInstance”
示例:
class Logger {constructor(name) {
this.name = name;
this.logs = [];
}
static instance = null;
static getInstance(name = 'default') {
if (!Logger.instance) {
Logger.instance = new Logger(name);
}
return Logger.instance;
}
log(msg) {
this.logs.push(`[${Date.now()}] ${msg}`);
}
}
const a = Logger.getInstance('app');
const b = Logger.getInstance('api');
console.log(a === b); // true
a.log('start');
console.log(b.logs.length); // 1
配合模块导出,天然保证模块级单例
在 ES 模块环境下(.mjs 或现代打包工具),直接导出一个实例比在 class 内部管理更自然、更可靠——因为模块本身就被运行时缓存,导入多次也只执行一次初始化。
- 不暴露 class,只导出已创建的实例
- 初始化逻辑写在模块顶层,支持异步准备(如读取配置)、依赖注入等
- 避免任何误用
new的可能,语义更干净
示例(logger.js):
class Logger {constructor(name) {
this.name = name;
this.logs = [];
}
log(msg) { this.logs.push(msg); }
}
// 模块级单例:只导出一个实例
export const logger = new Logger('main');
补充:避免常见误区
有些写法看似用了 class,实则没做到真正单例:
- 每次调用
getInstance都new一次 → 不是单例 - 把
instance声明在全局或函数外(非模块/静态作用域)→ 多模块间可能冲突 - 仅靠
new.target或构造器内检查this.constructor.instance→ 不可靠,且无法阻止外部new - 忘记提供销毁或重置机制(如测试中需清理状态)→ 可能引发内存泄漏或状态污染
本质上,class 语法只是组织代码的工具;单例的“唯一性”来自你对实例生命周期的主动管理,而不是语法本身。


















