JS 代码技巧 vol.9 --- 20 个设计模式在真实项目里的应用
小不的代码技巧系列第 9 期 主题:设计模式实战 这次盘 20 个------每个都对应一个真实 GitHub 仓库的具体源码,看完你也能在项目里复用 😎 20 个技巧一次打包,分创建型、结构型、行为型、其他四组。
哈喽哇!我是小不 ,不简说的不~
作为一个在代码界"翻车"无数次的选手,我算是看明白了:坑这东西吧,要么不踩,踩就踩大的😂
今天这期,主打一个 设计模式实战------不整虚的,全是真实 GitHub 项目里扒出来的实战用法。看不看随你~反正翻车实录又不收钱
Ps:文末有惊喜(不是广告!)
🚨 翻车名场面(不用设计模式的代价)
以下是真实项目里"裸写"出来的反例------作者已跑路,bug 已遗留
名场面 1:把全局变量当单例用
js
// ❌ 全局对象 = "我的"单例
window.myConfig = {
api: "https://api.example.com",
timeout: 5000,
};
// 同事 A 改了 api,同事 B 改了 timeout
// 排查:"我刚跑还是好的?" ------ 因为你俩改的
名场面 2:一个函数搞定 5 种业务
js
// ❌ 上古神兽"万能函数"
function handle(type, data) {
if (type === "login") {
/* 200 行 */
} else if (type === "logout") {
/* 200 行 */
} else if (type === "pay") {
/* 200 行 */
} else if (type === "refund") {
/* 200 行 */
} else if (type === "comment") {
/* 200 行 */
}
}
// 加新业务?Ctrl+C/V 吧(叹气)
名场面 3:3 个 tab 抢同一个全局事件
js
// ❌ 谁都监听,谁都改
window.addEventListener("user-login", (e) => {
// tab A 更新头像
// tab B 更新通知
// tab C 更新购物车
// 顺序乱、数据乱、调试乱
});
看完这 3 个,是不是想"我项目里也这样"------别慌,学完这期你就能开刀了。
一、创建型(4 个)
1. 单例模式:Pinia / Vue Router
一句话:整个应用只允许一个实例。
真实开源项目 :Pinia 是 Vue 3 官方推荐的状态库,每个 store 都是单例。
js
// Pinia 源码简化:packages/pinia/src/store.ts
export function defineStore(id, setup) {
// 全局缓存:同一个 id 只创建一次
if (!stores.has(id)) {
stores.set(id, createSetupStore(id, setup));
}
return stores.get(id);
}
// 业务用法
export const useUserStore = defineStore("user", () => {
const name = ref("小不");
return { name };
});
// 任何组件 import useUserStore(),拿到的是同一个实例
自己用 :登录用户、主题、全局配置------只该有一份的东西都用单例。
坑在哪 :单例的"全局状态"会让测试变难(互相污染),必要时提供 reset 方法 。前端单例用 ES Module 天然就够了,别再写 class 加 getInstance。
2. 工厂模式:React.createElement
一句话:调用一个函数,根据参数返回不同对象------不暴露创建细节。
真实开源项目 :React 的 React.createElement 是经典工厂。
js
// React 源码:packages/react/src/ReactElement.js
export function createElement(type, config, children) {
return {
$$typeof: REACT_ELEMENT_TYPE,
type,
props: { ...config, children },
// ...
};
}
// 你写的 JSX
const el = (
<div className="box" onClick={fn}>
Hello
</div>
);
// 编译后 = React.createElement('div', { className: 'box', onClick: fn }, 'Hello')
自己用:UI 组件库的"创建组件"函数、tooltip/popover 的"创建浮层"。
js
function createToast(options) {
return { id: Date.now(), type: "info", ...options };
}
const t1 = createToast({ message: "保存成功" });
const t2 = createToast({ type: "error", message: "网络错误" });
坑在哪 :工厂返回的对象结构要稳定 (React 元素就是固定结构),不然下游逻辑全乱。简单工厂 + 命名约定 > 复杂抽象。
3. 策略模式:dayjs / 插件系统
一句话:定义一族算法,运行时可互换。
真实开源项目 :dayjs 的插件机制就是策略模式。
js
// dayjs 源码:src/index.js
dayjs.extend(utcPlugin); // 策略 1:UTC
dayjs.extend(relativeTime); // 策略 2:相对时间
dayjs.extend(customParseFormat); // 策略 3:自定义解析
// 用法:根据加载的插件决定 dayjs() 行为
const now = dayjs();
now.utc().format(); // UTC 模式
now.fromNow(); // 相对时间(如果加载了插件)
自己用 :表单验证、支付方式、动画曲线、压缩算法------if-else 套娃的克星。
js
const validators = {
email: (v) => /\S+@\S+\.\S+/.test(v),
phone: (v) => /^1\d{10}$/.test(v),
idCard: (v) => /(^\d{15}$)|(^\d{17}([0-9]|X)$)/.test(v),
};
function validate(type, value) {
return validators[type]?.(value) ?? false;
}
坑在哪 :策略表用 key-value 形式而不是 switch ------加新策略不动旧代码。TS 项目给 key 加类型约束避免拼错。
4. 抽象工厂:UI 组件库 antd / element-plus
一句话:工厂的工厂------创建"一族相关对象"。
真实开源项目 :Ant Design 一键切换中英文,靠的就是 i18n 抽象工厂。
js
// Ant Design 源码:components/locale-provider/
import zhCN from "./locale/zh_CN";
import enUS from "./locale/en_US";
const locales = { zhCN, enUS };
export function getLocale(name) {
// 抽象工厂:根据 name 返回一整套本地化配置
return locales[name] ?? locales.zhCN;
}
// 用法:ConfigProvider 切换整套文案
<ConfigProvider locale={getLocale("enUS")}>
<App />
</ConfigProvider>;
自己用:多主题(暗/亮)、多平台(H5/小程序/原生)、多租户 UI 适配。
坑在哪 :抽象工厂容易过度设计 ------业务只有 2 个变体就别上。先看变化维度,再决定要不要抽象。
二、结构型(5 个)
5. 适配器模式:axios adapter
一句话:把一个接口"翻译"成另一个接口。
真实开源项目 :axios 的 adapter 设计堪称教科书。
js
// axios 源码:lib/adapters/index.js
const adapters = {
http: require("./adapters/http"), // 浏览器 fetch / Node http
xhr: require("./adapters/xhr"), // 浏览器 XMLHttpRequest
};
// 自动选择
function getDefaultAdapter() {
if (typeof XMLHttpRequest !== "undefined") return adapters.xhr;
if (typeof process !== "undefined") return adapters.http;
}
// 用户也能注入自己的 adapter(微信小程序、鸿蒙、Service Worker...)
const instance = axios.create({
adapter: wxRequestAdapter, // 适配微信小程序
});
自己用:跨端代码(H5 → 小程序 → 桌面端)、新旧接口过渡、第三方 SDK 包装。
坑在哪 :adapter 接口要稳定 (axios 的 adapter(config) 返回 Promise),下游才能透明切换。
6. 装饰器模式:MobX / TC39 装饰器提案
一句话:不修改原对象,动态加功能。
真实开源项目 :MobX 大量用装饰器(也是 TC39 提案的主要推动者)。
js
// MobX 用法
import { observable, computed, action } from "mobx";
class TodoStore {
@observable list = []; // 装饰:自动追踪
@computed get unfinished() {
// 装饰:自动缓存
return this.list.filter((t) => !t.done);
}
@action add(text) {
// 装饰:批处理
this.list.push({ text, done: false });
}
}
// 等价非装饰器写法
class TodoStore {
list = observable.box([]);
// ...
}
自己用:AOP 切面(日志、鉴权、计时)、React HOC、高阶函数本身就是装饰器。
js
function withLog(fn) {
return (...args) => {
console.log("调用:", fn.name, args);
const result = fn(...args);
console.log("返回:", result);
return result;
};
}
坑在哪 :装饰器改变 fn.name ,调试栈追踪可能丢名字。TC39 装饰器还在提案阶段,Babel 还在转译层(Stage 3),生产看目标浏览器再决定用不用。
7. 代理模式:Vue 3 reactivity
一句话:用一个对象"代理"另一个对象,控制访问。
真实开源项目 :Vue 3 的 reactivity 是 Proxy 的教科书用法。
js
// Vue 3 源码:packages/reactivity/src/reactive.ts
export function reactive(target) {
// 已经是 reactive 的直接返回(避免重复代理)
if (isReactive(target)) return target;
return createReactiveObject(
target,
mutableHandlers, // 拦截 get/set/deleteProperty 等
reactiveMap,
);
}
// mutableHandlers 简化
const mutableHandlers = {
get(target, key, receiver) {
track(target, key); // 依赖收集
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver);
trigger(target, key); // 触发更新
return result;
},
};
自己用:只读视图、属性校验、对象快照、Vue 响应式。
坑在哪 :Proxy 不能完全替代 defineProperty (Vue 2 时代)------Proxy 不代理不存在的属性。深代理要递归,浅代理 = 半吊子。
8. 外观模式:jQuery / lodash chain
一句话:把多个底层 API 组合成"统一好用的门面"。
真实开源项目 :lodash 的 _.chain 是外观模式。
js
// lodash chain 用法
const result = _(users)
.filter((u) => u.active)
.map((u) => u.name)
.sortBy()
.take(5)
.value();
// 一行完成 5 步操作,链式清爽
自己用:复杂 SDK 包装(地图、支付、IM)、统一团队工具方法。
js
// 业务里的"业务工具箱"
const _ = {
formatDate: (d) => dayjs(d).format("YYYY-MM-DD"),
formatMoney: (n) => n.toFixed(2),
safeJSON: (s, d = null) => {
try {
return JSON.parse(s);
} catch {
return d;
}
},
// ...
};
坑在哪 :外观别太"全"------utils 啥都装 = 啥都不好找。按业务域分工具包。
9. 享元模式:虚拟列表
一句话:共享细粒度对象,减少内存。
真实开源项目 :react-virtual / vue-virtual-scroller 都是享元。
js
// 思路:只渲染可视区内的 row
const visible = list.slice(startIdx, endIdx);
// 不可见的 row DOM 节点被回收(享元:节点对象复用)
自己用:长列表、地图 marker、canvas 粒子系统。
坑在哪 :享元的"对象池"管理复杂,业务不极致追求性能别上 。React 用 key 复用元素,Vue 用 <keep-alive> 缓存组件------框架帮你做了。
三、行为型(8 个)
10. 观察者模式:Node.js EventEmitter
一句话:对象状态变化时通知所有依赖者。
真实开源项目 :Node.js 内置 events 模块,每个流都基于它。
js
// Node 源码:lib/events.js
class EventEmitter {
on(event, listener) {
/* 注册 */
}
emit(event, ...args) {
/* 触发 */
}
off(event, listener) {
/* 解绑 */
}
}
// 用法
server.on("request", (req, res) => {
/* 处理请求 */
});
process.on("uncaughtException", (err) => {
/* 兜底 */
});
自己用:Node 流、文件监听、自定义事件总线。
坑在哪 :off 漏写 = 内存泄漏元凶 。最大监听数默认 10------超了 Node 会警告"possible memory leak"。
11. 发布订阅:mitt
一句话:发布者和订阅者通过"事件中心"解耦。
真实开源项目 :mitt 是个 200 行的发布订阅库。
js
// mitt 源码:src/index.ts
export default function mitt(all = new Map()) {
return {
on(type, handler) {
/* ... */
},
off(type, handler) {
/* ... */
},
emit(type, event) {
/* ... */
},
};
}
// 用法
const bus = mitt();
bus.on("login", (user) => console.log(user.name, "登录"));
bus.emit("login", { name: "小不" });
// 跟观察者的区别:发布者不知道订阅者是谁(解耦)
自己用 :跨组件通信、跨 tab 通信、BroadcastChannel 包装。
坑在哪 :事件名用常量别裸字符串------TS 联合类型防拼错。
12. 中间件模式:Koa / Redux
一句话:请求/动作通过"管道"层层处理。
真实开源项目 :Koa 的"洋葱模型"是中间件范式。
js
// Koa 源码:lib/application.js
function compose(middleware) {
return function (context, next) {
let index = -1;
return dispatch(0);
function dispatch(i) {
if (i <= index)
return Promise.reject(new Error("next() called multiple times"));
index = i;
let fn = middleware[i];
if (i === middleware.length) fn = next;
if (!fn) return Promise.resolve();
try {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err);
}
}
};
}
// 业务用法:鉴权 → 日志 → 业务 → 错误处理
app.use(authMiddleware);
app.use(loggerMiddleware);
app.use(errorHandler);
自己用:HTTP 请求管道、Redux 中间件、Express 路由。
坑在哪 :next() 调多次 = 报错;洋葱模型 = 先进后出,日志中间件"前记录 + 后记录"配对写。
13. 命令模式:Redux actions
一句话:把"请求"封装成对象,可以排队、撤销、记录。
真实开源项目 :Redux 的 action 是命令模式。
js
// Redux 用法
const action = { type: "ADD_TODO", payload: { text: "学设计模式" } };
dispatch(action);
// 撤销:dispatch 反向 action
const undo = { type: "UNDO" };
dispatch(undo);
// 记录:Redux DevTools 看所有 action 历史
自己用:撤销/重做、操作历史、异步任务队列。
坑在哪 :action 是纯数据 (不要在 action 里写业务逻辑)。reducer 才是业务的归宿。
14. 责任链模式:jQuery 事件冒泡 / Express
一句话:请求沿链传递,每个节点决定"处理 or 传给下一个"。
真实开源项目 :jQuery 事件委托是经典责任链。
js
// jQuery on() 实现
$(document).on("click", ".btn", handler);
// 事件从 document 向下冒泡(或捕获向上)
// 任何一层可以 stopPropagation 截断
自己用:表单字段校验(依次校验每个字段)、权限校验链、Express 中间件。
坑在哪 :链太长 = 调试地狱。每个节点只做一件事,别越权。
15. 迭代器模式:Generator / Array.from
一句话:顺序访问集合元素,不暴露内部结构。
真实开源项目 :JavaScript 内置 Symbol.iterator + Babel 编译器大量用。
js
// Array 自带迭代器
const arr = [1, 2, 3];
const iter = arr[Symbol.iterator]();
iter.next(); // { value: 1, done: false }
iter.next(); // { value: 2, done: false }
// 自定义迭代器(Generator)
function* fib() {
let [a, b] = [0, 1];
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
const f = fib();
f.next(); // { value: 0, done: false }
f.next(); // { value: 1, done: false }
自己用:流式数据、无限序列、懒加载列表。
坑在哪 :迭代器只能遍历一次 。别手写迭代器,Generator 是甜点。
16. 状态模式:XState
一句话:对象行为随状态改变而改变。
真实开源项目 :XState 是正经的状态机库。
js
// XState 用法
import { createMachine } from "xstate";
const lightMachine = createMachine({
initial: "green",
states: {
green: { on: { TIMER: "yellow" } },
yellow: { on: { TIMER: "red" } },
red: { on: { TIMER: "green" } },
},
});
// 状态转换可视化、可测试
自己用:订单状态(待支付/已支付/已发货/已退款)、播放器状态、表单步骤。
坑在哪 :状态机**适合"状态有限且明确"**的场景,别把整个业务塞状态机------简单 if-else 足够就别上。
17. 备忘录模式:Redux DevTools
一句话:保存对象状态,必要时恢复(撤销/快照)。
真实开源项目 :Redux DevTools Extension 时间旅行调试 = 备忘录模式。
js
// 每次 dispatch 都保存完整 state
const stateHistory = [initialState, state1, state2, ...];
// DevTools 可以"穿越"到任意历史 state
store.dispatch(action);
console.log(stateHistory); // 完整时间线
自己用:撤销/重做、表单"草稿"、画板的"快照"。
坑在哪 :备忘录吃内存 ------大型 state 别每个都快照。structuredClone 比 JSON 快又稳。
18. 访问者模式:Babel AST 遍历
一句话:分离算法和对象结构,在不修改对象的前提下新增操作。
真实开源项目 :Babel 编译器遍历 AST 用访问者。
js
// Babel 用法
const visitor = {
Identifier(path) {
console.log("发现标识符:", path.node.name);
},
BinaryExpression(path) {
console.log("二元运算:", path.node.operator);
},
};
babel.transform(code, { plugins: [{ visitor }] });
自己用:代码检查工具(ESLint)、AST 转换、报表数据处理。
坑在哪 :访问者模式对 AST 友好但对业务过重 。别为了"模式"强行套。
四、其他常用(2 个)
19. 组合模式:React / Vue 组件树
一句话:把对象组合成树形结构,"部分-整体"统一处理。
真实开源项目 :React 组件树就是组合模式。
jsx
<Card>
<Header>
<Avatar />
<Title>小不</Title>
</Header>
<Body>
<Text>...</Text>
<Button>点我</Button>
</Body>
</Card>
// 整棵树统一 props 传递、统一事件冒泡
自己用:UI 组件树、菜单、文件树、组织架构树。
坑在哪 :组合的"叶子"和"枝干"接口要一致(React 的 props.children),不然难统一渲染。
20. 解释器模式:模板引擎(Handlebars / ejs)
一句话:给定一个语言,定义它的文法表示和解释器。
真实开源项目 :Handlebars / EJS 都是解释器。
js
// Handlebars 用法
const template = Handlebars.compile("你好,{{name}}!");
template({ name: "小不" }); // "你好,小不!"
// 条件、循环、子模板------都靠解释器解析
自己用:模板引擎、DSL 配置、业务规则引擎。
坑在哪 :解释器模式实现复杂 ,业务里几乎用不到手写------用现成库就行。
📦 收个尾
这 20 个设计模式浓缩一下:
- 最高频踩坑 :全局变量当单例、把 if-else 嵌套当工厂、责任链传错
next死循环 - 最值得收藏:Vue 3 Proxy 响应式、Koa 中间件洋葱模型、axios adapter 跨端
- 核心思路 :模式不是炫技,是被问题逼出来的------能拍扁 if-else 就拍扁,但别为了"模式"强行套
学到了就是赚到了,犹豫徘徊等于白来~
写到最后
想要啥技巧?评论区甩个题目过来~
- 你刚踩的坑
- 项目里反复写的代码
- 想搞清楚但一直懒得查的 API
小不看到...不一定回 😂 毕竟代码里翻车太多,腾不出手~
Ps:三连随缘,催更的会被打 😂