你有没有写过这种代码:在三个不同的页面里,分别用 if/else 判断当前环境,然后创建不同的 service 实例?
第一次写的时候没觉得有问题。等第四个页面也需要同样的判断,你才意识到:这些分支逻辑散落在四个地方,改一个漏三个。
这就是工厂模式要解决的核心问题——不是"怎么创建对象",而是 "谁来管创建对象时的分支判断" 。
一、工厂模式到底在做什么
patterns.dev 上对工厂模式的定义很简洁:一个函数返回新对象,不使用 new 关键字。
const createUser = ({ firstName, lastName, email }) => ({
firstName,
lastName,
email,
fullName() {
return `${this.firstName} ${this.lastName}`;
},
});
看起来平平无奇——就是个返回对象的函数嘛。
但"平平无奇"恰恰是重点。它的价值不在语法本身,而在于它提供了一个受控的创建边界。当你的系统需要根据不同条件创建不同实现时,工厂就是那个唯一的"接线盒":所有创建分支汇聚于此,业务代码只管拿到结果,不关心里面接了哪根线。
工厂的核心价值不是简化创建,是集中管理创建时的分支判断。
二、没有工厂时,分支散落成什么样
想象一个前端项目需要根据运行环境(开发 / 测试 / 生产)使用不同的 API service:
// ❌ 页面 A
const api = env === 'dev' ? new MockApi() : new RealApi();
// ❌ 页面 B(复制粘贴了一份)
const api = env === 'dev' ? new MockApi() : new RealApi();
// ❌ 页面 C(又复制了一份,还漏了 test 环境)
const api = env === 'dev' ? new MockApi() : new RealApi();
三个页面,三份一模一样的判断逻辑。有人加了 staging 环境?改三个地方。有人漏改一个?线上出 bug。
用工厂收拢之后:
// ✅ 工厂:所有分支只在这里
function createApiService(env) {
switch (env) {
case 'dev': return new MockApi();
case 'test': return new MockApi({ delay: 0 });
case 'staging': return new StagingApi();
default: return new RealApi();
}
}
// 页面 A / B / C 都只写一行
const api = createApiService(currentEnv);
分支逻辑从"散落在 N 个消费方"变成"集中在 1 个工厂"。加环境、改逻辑,只动一个地方。
这就像大型仓储的分拣中心——车间不会自己跑到仓库翻零件,分拣中心统一接单、按需配送。工厂就是代码世界的分拣中心:内部调整分配规则,车间(业务代码)完全不用改。
三、前端最常见的四种工厂场景
工厂模式不是后端专利。前端项目里,以下四种场景几乎一定会遇到:
| 场景 | 工厂负责什么 | 典型例子 |
|---|---|---|
| 组件工厂 | 根据类型/配置返回不同组件 | 动态表单:text → TextField,select → SelectField |
| Service 工厂 | 根据环境/平台返回不同服务实现 | 开发用 MockApi,生产用 RealApi |
| Adapter 工厂 | 根据第三方依赖返回统一接口的适配器 | 高德地图 / Google Maps 统一渲染接口 |
| Tool Client 工厂 | 根据配置返回不同工具链客户端 | AI agent pipeline 中按模型名返回不同 LLM client |
组件工厂在 React 中特别常见。与其在 JSX 里写一堆 if/else,不如用一张映射表充当工厂:
const FIELD_MAP = {
text: TextField,
select: SelectField,
checkbox: CheckboxField,
};
function createField(type, props) {
const Component = FIELD_MAP[type] || TextField;
return <Component {...props} />;
}
这张映射表就是最轻量的工厂——加新类型只需加一行映射,不改任何业务代码。
工厂的扩展方式是"加一行注册",而不是"改 N 处 if/else"。
四、工厂函数 vs class:一笔 trade-off 账
patterns.dev 原文特别强调了一个常被忽略的区别:内存效率。
工厂函数每次调用都返回一个全新对象,对象上的方法是各自独立的副本:
const u1 = createUser({ firstName: 'A', lastName: 'B', email: 'a@b.com' });
const u2 = createUser({ firstName: 'C', lastName: 'D', email: 'c@d.com' });
u1.fullName === u2.fullName; // false — 两份不同的函数
而 class 的方法存在 prototype 上,所有实例共享同一份引用:
const u3 = new User('A', 'B', 'a@b.com');
const u4 = new User('C', 'D', 'c@d.com');
u3.fullName === u4.fullName; // true — 原型上同一个函数
创建 10 个对象感受不到差异。创建 10000 个,内存差距就出来了。
但反过来,工厂函数也有 class 给不了的东西:
| 维度 | 工厂函数 | class |
|---|---|---|
| 内存效率 | ❌ 每实例复制方法 | ✅ 原型共享 |
| this 安全 | ✅ 闭包捕获,不怕丢失 | ❌ 解构/回调中 this 易丢 |
| 封装能力 | ✅ 闭包天然私有 | ✅ # 私有字段(较新) |
| 组合灵活性 | ✅ 自由混入 | ⚠️ 单继承限制 |
| instanceof | ❌ 不支持 | ✅ 支持 |
经济学有个核心概念叫机会成本——你选择 A 的代价就是放弃 B。工厂函数用内存换来了 this 安全和封装便利;class 用 this 的心智负担换来了内存效率。
好的工程决策不是选"最好的",是选"在当前约束下机会成本最低的"。
实际判断很简单:
• 轻量对象、数量有限、需要传回调 → 工厂函数
• 重方法复用、大量实例、需要继承体系 → class
• React 组件工厂 → 映射表 + 函数(最符合 React 的函数式风格)
五、工厂模式的边界在哪
工厂不是万能药。两种场景不需要它:
场景一:类型固定且少。 只有两种按钮(主要 / 次要),直接 isPrimary ? <Primary /> : <Secondary /> 就行。为两个分支建工厂是过度设计。
场景二:创建逻辑本身就很简单。 new User(name, email) 没有任何分支判断,套一层 createUser 只增加了一层无意义的间接性。
patterns.dev 原文也坦率地说:在 JavaScript 中,工厂模式本质上"just a function that returns an object"——如果没有分支判断需要集中管理,它就只是一个普通函数,没有额外的设计价值。
判断是否需要工厂的信号很明确:
| 信号 | 该怎么做 |
|---|---|
同样的 if/else 创建逻辑出现在 ≥ 2 处 | ✅ 收拢到工厂 |
| 创建时需要根据环境/配置/类型做分支 | ✅ 工厂是天然边界 |
| 只有 1-2 个固定类型,无扩展预期 | ❌ 直接条件表达式 |
创建逻辑无分支,只是 new Xxx() | ❌ 不需要工厂 |
这和家里的电器插座一样——你不需要为每个电器都配一个转接头工厂。但如果你要带一箱电器去五个国家,一个能根据国家参数自动匹配制式的转接头工厂,就是刚需。
六、工厂在架构决策链中的位置
如果把最近几篇文章串起来,你会发现工厂模式刚好填补了一个缺口:
| 原则 / 模式 | 回答什么问题 |
|---|---|
| SoC(关注点分离) | 这段逻辑应该放在哪一层? |
| SRP(单一职责) | 这个模块应该只干一件事 |
| DI / DIP(依赖反转) | 依赖应该指向谁? |
| AHA(避免草率抽象) | 什么时候该提炼抽象? |
| Factory(工厂模式) | "创建不同实现"这个分支判断,应该由谁管? |
SoC 和 SRP 告诉你把创建逻辑从业务逻辑里分出来;DI 告诉你消费方不该直接依赖具体实现;AHA 告诉你别急着抽象——先确认分支确实在多处重复。
工厂是这条决策链的最后一环:当你确认了边界、方向、时机,工厂就是那个把"创建谁"集中到一处的接线盒。
如果你只想带走一句话,我建议记这个:
工厂的价值不是让创建更简洁,而是让"创建时的分支判断"只存在于一个地方。散落的 if/else 是定时炸弹,集中的工厂是保险丝。
参考原文:
• patterns.dev — Factory Pattern