怎么在ES6 Class中实现JavaScript工厂模式
本文系统讲解怎么在 ES6 Class 中实现 JavaScript 工厂模式,涵盖静态方法分发、Map 动态注册表、异步工厂及抽象工厂模式的完整代码与架构解析。
在 ES6 中实现工厂模式,本质上是通过类(Class)或其静态方法(static)封装具有共性的子类实例化细节,使外部调用方仅通过传递类型标识或配置参数,就能直接获取具备统一契约的产品实例,而无需在业务代码各处直接书写 new 具体子类()。这种方式不仅解除了业务逻辑与具体实现的紧密耦合,还能集中管理依赖注入、全局配置以及多态实例的分发。
一、为什么要在 ES6 Class 中使用工厂模式?
在 ES6 引入原生 class 语法之前,JavaScript 开发者通常使用普通函数和原型链返回纯字面量对象或构造器实例。有了 Class 语法后,面向对象规范更为直观,但直接在业务逻辑中到处调用 new ConcreteClass() 会暴露出三个典型痛点:
- 依赖强耦合:调用方必须事先了解每一个具体子类的类名、引入路径以及构造参数的细微差异。
- 缺乏扩展弹性:当业务需要根据后端返回字段动态切换底层对象(如切换短信通道或支付渠道)时,散落在各处的
switch-case或if-else会导致每次变更都要修改多处调用代码,违反开闭原则(OCP)。 - 难以统一拦截预处理:直接使用
new无法在对象返回前做全局校验、日志审计、单例复用或统一属性注入。
引入工厂类后,调用方只与工厂类以及抽象基类定义的公用接口交互。工厂类充当调度中枢,具体子类各自继承自公用基类并实现标准化行为,结构关系如上图所示。
二、核心实现方式一:静态简单工厂与动态注册表
最直接也是工程中最常用的实现方式,是在工厂类中提供静态方法 static create(type, options)。为了避免在静态方法中堆砌冗长的 switch-case 语句,标准做法是使用 Map 维护一套类型映射注册表,使工厂具备动态扩展能力。
1. 定义抽象产品基类与具体子类
首先定义规范各子类公共行为的基类 BaseNotification,并利用 new.target 模拟抽象类,防止基类被直接意外实例化:
// 抽象产品基类
class BaseNotification {
constructor(config) {
if (new.target === BaseNotification) {
throw new Error('BaseNotification 是抽象基类,不能直接实例化');
}
this.recipient = config.recipient;
this.content = config.content;
}
// 统一约定必须实现的抽象方法
send() {
throw new Error('子类必须实现 send() 方法');
}
}
// 具体产品子类 A:邮件通知
class EmailNotification extends BaseNotification {
constructor(config) {
super(config);
this.subject = config.subject || '系统邮件';
}
send() {
return `[邮件] 发送给 ${this.recipient},标题: ${this.subject},正文: ${this.content}`;
}
}
// 具体产品子类 B:短信通知
class SMSNotification extends BaseNotification {
constructor(config) {
super(config);
this.signName = config.signName || '系统通知';
}
send() {
return `[短信] 【${this.signName}】发送给 ${this.recipient}:${this.content}`;
}
}
2. 编写具备注册功能的工厂类
工厂类通过私有或静态 Map 存储具体的类构造器。外部既可以直接使用内置预设,也能在后续运行阶段动态注册新子类:
class NotificationFactory {
// 使用静态 Map 维护类型标识与类引用的映射
static registry = new Map();
// 注册新类:实现对修改关闭、对扩展开放
static register(type, classConstructor) {
if (typeof classConstructor !== 'function') {
throw new TypeError('注册的目标必须为类或构造函数');
}
this.registry.set(type.toLowerCase(), classConstructor);
}
// 核心创建方法
static create(type, config) {
const normalizedType = String(type).toLowerCase();
const TargetClass = this.registry.get(normalizedType);
if (!TargetClass) {
throw new Error(`未注册的通知类型: ${type},当前可用: ${Array.from(this.registry.keys()).join(', ')}`);
}
// 可以在此统一进行参数补全、环境检测或埋点
const finalConfig = { ...config, timestamp: Date.now() };
return new TargetClass(finalConfig);
}
}
// 预置标准产品类
NotificationFactory.register('email', EmailNotification);
NotificationFactory.register('sms', SMSNotification);
3. 实际调用与动态扩展效果
业务代码现在只需统一面向 NotificationFactory.create 进行调用。如果后续需要新增站内推送通知(Push),不需要更改原有工厂方法逻辑,只需要调用 register 即可生效:
// 业务调用
const emailNotice = NotificationFactory.create('email', {
recipient: 'admin@example.com',
subject: '监控告警',
content: 'CPU 使用率超过 90%'
});
console.log(emailNotice.send());
// 运行时动态扩展:定义新类并注册
class PushNotification extends BaseNotification {
send() {
return `[站内推送] 向设备 ${this.recipient} 推送: ${this.content}`;
}
}
// 无需修改 NotificationFactory 原代码,直接注册
NotificationFactory.register('push', PushNotification);
const pushNotice = NotificationFactory.create('push', {
recipient: 'device_token_8899',
content: '新版本已发布'
});
console.log(pushNotice.send());
三、核心实现方式二:工厂方法模式(Factory Method)
简单工厂将所有对象的构建逻辑集中在单个类中,当不同产品的实例化步骤(如鉴权准备、网络连接、参数解析)差异极大时,工厂方法模式更为合适。工厂方法模式的核心是将“创建具体产品”的动作延迟到子类工厂中。
// 抽象工厂基类
class LoggerFactory {
constructor() {
if (new.target === LoggerFactory) {
throw new Error('LoggerFactory 为抽象工厂,不可直接实例化');
}
}
// 抽象工厂方法:要求子类重写
createLogger() {
throw new Error('子类工厂必须实现 createLogger() 方法');
}
// 模版方法:定义公共业务流程
writeLog(message) {
const logger = this.createLogger();
logger.log(message);
}
}
// 具体工厂 A:文件日志工厂
class FileLoggerFactory extends LoggerFactory {
constructor(filePath) {
super();
this.filePath = filePath;
}
createLogger() {
// 可以包含专属于文件日志的复杂初始化
return {
log: (msg) => console.log(`[File: ${this.filePath}] ${msg}`)
};
}
}
// 具体工厂 B:控制台日志工厂
class ConsoleLoggerFactory extends LoggerFactory {
createLogger() {
return {
log: (msg) => console.log(`[Console] ${msg}`)
};
}
}
// 使用方式
const fileFactory = new FileLoggerFactory('/var/log/app.log');
fileFactory.writeLog('数据库连接成功');
四、解决原生限制:静态异步工厂(Async Factory)
在 JavaScript ES6 Class 中,类的 constructor 必须是同步执行的,语法上不允许使用 async constructor()。然而在实际业务中,产品实例的构建往往依赖异步前置准备(例如异步读取本地证书、拉取远程鉴权凭证或建立 Socket 握手)。
工厂模式的静态方法天然支持 async/await,是解决这一语言限制的标准方案:
class DatabaseConnection {
// 构造函数设为保护视角,只接收准备完备的参数
constructor(rawConnection, credentials) {
this.conn = rawConnection;
this.auth = credentials;
}
// 静态异步工厂方法
static async create(config) {
// 1. 异步获取凭证或解析 DNS
const credentials = await fetchRemoteAuth(config.authUrl);
// 2. 异步建立网络握手
const rawConn = await openSocket(config.host, credentials.token);
// 3. 握手完成后再调用内部构造函数返回实例
return new DatabaseConnection(rawConn, credentials);
}
query(sql) {
return this.conn.execute(sql);
}
}
// 模拟异步辅助函数
async function fetchRemoteAuth(url) {
return { token: 'auth_tok_xyz123' };
}
async function openSocket(host, token) {
return { execute: (s) => `执行结果: ${s} (主机: ${host})` };
}
// 调用方式:优雅解决异步初始化问题
(async () => {
const db = await DatabaseConnection.create({
host: 'db.internal.net',
authUrl: 'https://auth.internal.net/get'
});
console.log(db.query('SELECT * FROM users'));
})();
static async create() 中,既保证了返回的实例是完全初始化完毕的健康对象,又避免了调用方在创建对象后还要手动调用一次 instance.init(),规避了状态不一致隐患。
五、工程实践中的避坑指南
在 ES6 Class 中落地工厂模式时,需要特别注意以下细节,避免引入不必要的维护负担:
| 潜在隐患 | 现象与风险 | 最佳应对策略 |
|---|---|---|
| 过度设计与模式滥用 | 只有一两个稳定且构造极简的类,却套用三层工厂结构。 | 单一类型或无需多态时直接使用 new Class(),无需强制引入工厂。 |
| 缺乏接口契约检查 | 由于 JavaScript 没有编译期 interface,注册了缺少公共方法的类。 |
在 factory.register() 阶段检查传入类原型是否包含约定方法,及时报错。 |
| 未处理未知类型异常 | 传入无效类型名时返回 undefined,导致后续链式调用爆出“cannot read properties of undefined”。 |
在工厂内部严格校验并显式抛出语义明确的 Error,方便排查定位。 |
| 单例与多例混淆 | 将带状态的类在工厂中缓存并重复返回,导致不同请求间数据相互污染。 | 工厂默认应创建全新实例;若需单例,显式使用专用缓存字典并明确命名方法。 |
六、总结
在 ES6 Class 中实现工厂模式的关键不是死记模式术语,而是利用 JavaScript 类的语法特性为对象生命周期建立清晰的边界:
- 对于日常项目中的类型分发场景,优先采用静态方法配合注册表(Static Factory + Map),能以极小的代码成本带来优秀的解耦与扩展性;
- 对于涉及网络鉴权、配置拉取等前置任务的对象,使用静态异步工厂方法(
static async create())是规避constructor语法限制的最佳实践; - 始终牢记接口统一性:工厂产出的所有子类实例应遵循相同的公开方法约定,使调用方无需关注具体实现类型,真正发挥多态代码的简洁与可靠优势。
JS读取本地文件时报错或无响应的常见原因
2026-11-05 16:32:31
let、const与var的区别:JavaScript变量声明最佳实践
2026-11-05 14:16:38
JS中直接定义静态二维数组的字面量写法
2026-11-05 14:16:08
JS 怎么查找字符串中所有匹配项的位置
2026-11-05 14:15:53
JavaScript新手常犯的编码错误与最佳实践指南
2026-11-04 22:12:38
相关标签:








