装饰器模式(Decorator)
1)用一个需求引入:我只想“加点功能”,不想改原代码
你在做前端业务时,经常会遇到这种场景:
- 已有一个函数
request()很稳定,很多地方在用 - 现在产品说:所有请求要自动加 loading、统计耗时、失败上报、权限校验
- 你不想(也不敢)到处改
request()的内部实现,更不想复制粘贴出requestWithLoading()、requestWithLog()、requestWithAuth()一堆变体
这时你需要一种方式:在不修改原有对象/函数的前提下,为它“叠加”额外能力。
这就是**装饰器模式(Decorator)**要解决的问题。
2)装饰器模式的关键点(简洁版)
装饰器模式核心就三句话:
- 不改原实现:原对象/函数保持不变(遵循开闭原则:对扩展开放、对修改关闭)
- 包装(wrap):用一个“装饰器”把原对象/函数包起来
- 可组合:多个装饰器可以像搭积木一样叠加,顺序不同效果可能不同
在 JS 里,装饰器模式最常见的体现就是:高阶函数(函数接收函数,返回新函数)或 包装对象。
3)简单代码例子:用高阶函数装饰一个函数
例子:给一个函数加“耗时统计”和“异常捕获”
假设我们有个核心函数:
function fetchUser(id) {
// 模拟业务逻辑
if (!id) throw new Error('id is required');
return { id, name: 'Alice' };
}
现在我们希望“装饰”它,而不是改它:
装饰器 1:统计耗时
function withTiming(fn, label = fn.name) {
return function (...args) {
const start = performance.now();
try {
return fn.apply(this, args);
} finally {
const cost = performance.now() - start;
console.log(`[timing] ${label}: ${cost.toFixed(2)}ms`);
}
};
}
装饰器 2:异常捕获并上报
function withErrorReport(fn, report = console.error) {
return function (...args) {
try {
return fn.apply(this, args);
} catch (err) {
report('[error]', err);
throw err; // 是否继续抛出取决于你的业务策略
}
};
}
组合使用(装饰 = 包一层再包一层)
const safeFetchUser = withErrorReport(withTiming(fetchUser, 'fetchUser'));
safeFetchUser(1); // 正常:打印耗时
safeFetchUser(); // 异常:上报 + 抛出 + 打印耗时
你会发现:
fetchUser本身完全没动- 新增能力通过包装完成
- 可以继续叠更多装饰器(缓存、鉴权、埋点……)
注意:装饰器的顺序会影响行为,比如先捕获异常还是先统计耗时,输出就可能不同。
4)常见应用场景(前端很常用)
场景 A:接口请求增强(最典型)
- 自动加 token
- 请求/响应日志
- loading 管理
- 重试机制
- 限流/防抖/节流
- 统一错误处理
场景 B:事件处理函数增强
- 点击埋点
- 防重复点击(节流)
- 权限校验(没权限直接 return)
- 自动
stopPropagation/preventDefault(谨慎使用)
场景 C:组件能力增强(思路一致)
在 React/Vue 生态里常见“包装增强”:
- React 的 HOC(高阶组件)本质上就是装饰器思想
- Vue 中对方法/生命周期进行统一增强(比如 mixin、插件机制)也有类似目的
场景 D:业务函数的横切能力(AOP 风格)
- 统一参数校验
- 统一日志、审计
- 性能统计
- 监控报警
5)注意事项(踩坑提醒)
-
装饰器别改变原函数语义
- 最好只做“横切关注点”(日志、计时、埋点),不要偷偷改返回值结构,否则很难排查问题。
-
注意 this 与参数透传
- 用
fn.apply(this, args),否则被装饰函数依赖this时会出 bug。
- 用
-
装饰链太长会降低可读性
- 过度包装会让调试栈变深、定位变难。必要时把多个装饰器合并成一个“组合装饰器”。
-
异步函数要特别处理
- 如果被装饰函数返回 Promise,计时/异常捕获要能处理
await场景(例如在装饰器里await fn(...)再计时/捕获)。
- 如果被装饰函数返回 Promise,计时/异常捕获要能处理
-
装饰顺序很关键
- 例如:
withAuth(withCache(fn))与withCache(withAuth(fn))的安全性/命中率可能完全不同。
- 例如: