学完这篇你能得到 10 种常用设计模式在 JavaScript 里的最小可用实现,知道每种模式解决什么问题、代码怎么写,以及实际使用中最容易出错的地方。
第 0 步:先记住一个前提
设计模式不是公式,而是对“某个反复出现的结构问题”的通用解法。判断要不要用,只看两点:变化点是否被隔离出来了,以及新增一种情况时要不要改动老代码。如果两条都不满足,直接写直白代码更好。
第 1 种:单例(Singleton)
保证全局只有一个实例,常用于配置中心、弹窗管理器。
const Config = (() => {
let instance = null;
const create = () => ({ env: 'prod', api: 'https://api.example.com' });
return { get: () => instance || (instance = create()) };
})();
Config.get() === Config.get(); // true
注意:ES Module 本身就是单例,export default {} 就够用了,不必手写闭包。手写版本在热更新、多入口打包时可能被实例化多次。
第 2 种:工厂(Factory)
把 new 和分支判断收进一个函数,调用方只关心拿到的对象能干什么。
function createUser(type) {
const map = {
admin: () => ({ type, perms: ['read', 'write', 'delete'] }),
guest: () => ({ type, perms: ['read'] }),
};
if (!Object.hasOwn(map, type)) throw new Error('未知用户类型: ' + type);
return map[type]();
}
注意:用对象当字典时一定要用 Object.hasOwn 或 Object.create(null) 判断键是否存在,否则 constructor、__proto__ 会被误当成合法类型。
第 3 种:策略(Strategy)
把一堆 if/else 换成表驱动,新增分支时只加一项。
const discounts = {
wechat: p => p * 0.95,
alipay: p => p * 0.98,
cash: p => p,
};
const pay = (type, price) => discounts[type](price);
第 4 种:观察者(Observer)
被观察者直接持有观察者列表,状态一变就逐个通知。
class Subject {
#list = [];
subscribe(fn) { this.#list.push(fn); return () => this.#list = this.#list.filter(f => f !== fn); }
notify(data) { this.#list.forEach(fn => fn(data)); }
}
第 5 种:发布订阅(Pub/Sub)
比观察者多一个中间事件中心,发布方和订阅方互不认识。
const bus = {
map: new Map(),
on(k, fn) { (this.map.get(k) || this.map.set(k, []).get(k)).push(fn); },
emit(k, ...args) { (this.map.get(k) || []).forEach(fn => fn(...args)); },
off(k, fn) { const a = this.map.get(k) || []; this.map.set(k, a.filter(f => f !== fn)); },
};
注意:订阅返回的取消函数一定要在组件卸载或页面销毁时调用,否则回调里引用的 DOM 和闭包变量无法被释放,单页应用里这是最常见的内存泄漏来源。
第 6 种:装饰器(Decorator)
不修改原函数,在外面包一层加行为,比如计时、重试、鉴权。
const withTiming = fn => (...args) => {
const t = performance.now();
const r = fn(...args);
console.log(`${fn.name} 耗时 ${performance.now() - t}ms`);
return r;
};
注意:@decorator 语法仍在标准推进中,编译配置不同行为也不同;生产项目里用上面这种高阶函数组合最稳妥。
第 7 种:代理(Proxy)
用 Proxy 拦截读写,实现缓存、校验、访问控制。
const cache = new Map();
const api = new Proxy({}, {
get(_, key) {
if (!cache.has(key)) cache.set(key, async () => (await fetch('/api/' + key)).json());
return cache.get(key);
},
});
注意:拦截 get 时不要再接用 target[key] 里的 this,内部方法要用 Reflect.get(target, key, receiver) 传 receiver,否则 this 会指向代理对象,容易出现奇怪问题。
第 8 种:适配器(Adapter)
把老接口或第三方接口统一成项目内部的标准形状。
const oldUser = { user_name: 'tom', user_age: 20 };
const adapt = u => ({ name: u.user_name, age: u.user_age });
第 9 种:命令(Command)
把操作封装成对象,天然支持撤销、重做、队列。
const cmd = {
do: () => list.push(item),
undo: () => list.splice(list.indexOf(item), 1),
};
第 10 种:责任链(Chain)
每个节点只判断自己关心的条件,处理不了就交给下一个。
function chain(...fns) {
return (ctx) => { for (const fn of fns) { const r = fn(ctx); if (r !== undefined) return r; } };
}
注意:责任链一定要有兜底节点或明确的返回值约定,否则条件都不命中时会静默返回 undefined,排查起来很费劲。
小结
- 单例防重复实例,工厂收
new,策略替 if/else,观察者和发布订阅做解耦。
- 装饰器包函数、代理拦读写、适配器转形状、命令可撤销、责任链拆判断。
- 判断标准只有两条:变化点是否隔离、加需求要不要改老代码。
- 真正踩坑的多在细节:字典原型污染、订阅未取消、
Proxy 的 this、责任链缺兜底。