装饰器模式
在不改变原对象的前提下,动态地给它添加新的职责。
装饰器是继承的替代方案。用继承扩展功能会导致类爆炸:要给按钮加日志、加埋点、加防抖,用继承就得写 LoggedButton、TrackedButton、LoggedTrackedDebouncedButton……而装饰器可以任意组合、运行时叠加。
一、JS 里的装饰器:高阶函数
JS 有头等函数,装饰器退化成"接收函数、返回增强后函数"的高阶函数,这是它最常见的形态。
// 日志装饰
const withLog = fn => function (...args) {
console.log(`调用 ${fn.name},参数:`, args);
const result = fn.apply(this, args);
console.log(`返回:`, result);
return result;
};
// 计时装饰
const withTiming = fn => function (...args) {
const start = performance.now();
const result = fn.apply(this, args);
console.log(`${fn.name} 耗时 ${performance.now() - start}ms`);
return result;
};
// 自由组合
const add = (a, b) => a + b;
const enhanced = withLog(withTiming(add));
写装饰器必须守住的三条
- 用
function而不是箭头函数包装,否则this会丢失(箭头函数没有自己的this,无法通过apply转发调用者的this)。 - 必须
return原函数的返回值,否则装饰后返回undefined——这是最常见的低级 bug。 - 保留函数元信息:包装后
fn.name变成''、fn.length变成0。依赖这些信息的框架(如某些依赖注入库)会失效,必要时手动复制:Object.defineProperty(wrapped, 'name', { value: fn.name });
异步函数要单独处理,否则计时和异常捕获都会失效:
const withAsyncTiming = fn => async function (...args) {
const start = performance.now();
try {
return await fn.apply(this, args); // 必须 await,否则计的是"发起"的时间
} finally {
console.log(`耗时 ${performance.now() - start}ms`);
}
};
二、AOP:给原型方法织入行为
面向切面编程(AOP)把日志、埋点、鉴权这些横切关注点从业务逻辑里剥离出来。
Function.prototype.before = function (beforeFn) {
const self = this;
return function (...args) {
beforeFn.apply(this, args); // 先执行前置逻辑
return self.apply(this, args);
};
};
Function.prototype.after = function (afterFn) {
const self = this;
return function (...args) {
const result = self.apply(this, args);
afterFn.apply(this, args);
return result; // 注意返回的是原函数的结果
};
};
let submit = function () { console.log('提交表单'); };
submit = submit.before(() => console.log('校验参数')).after(() => console.log('埋点上报'));
submit();
// 校验参数 → 提交表单 → 埋点上报
不要在生产代码里污染原型
修改 Function.prototype 是典型的猴子补丁:多个库同时这么做会互相覆盖,且 for...in 遍历、类型定义都可能受影响。上面写法适合理解原理,工程中请写成独立的工具函数:
const before = (fn, beforeFn) => function (...args) { beforeFn.apply(this, args); return fn.apply(this, args); };
一个真实用途:给已有方法打补丁而不改源码(比如统计所有路由跳转)。
const originalPush = router.push;
router.push = function (...args) {
track('route_change', args[0]);
return originalPush.apply(this, args);
};
三、TypeScript / ES 装饰器语法
装饰器提案已进入 ES Stage 3,TypeScript 5.0+ 支持新版标准语法(与旧的 experimentalDecorators 实现不兼容,注意区分)。
// 方法装饰器(TS 5.0+ 标准语法)
function logged<This, Args extends unknown[], Return>(
target: (this: This, ...args: Args) => Return,
context: ClassMethodDecoratorContext
) {
return function (this: This, ...args: Args): Return {
console.log(`调用 ${String(context.name)}`);
return target.call(this, ...args);
};
}
class Service {
@logged
fetchData(id: number) { return `data-${id}`; }
}
装饰器在框架里随处可见:Angular 的 @Component / @Injectable、NestJS 的 @Controller / @Get、MobX 的 @observable、TypeORM 的 @Entity。它们本质上都是在类定义时注册元数据,框架再据此完成依赖注入或路由绑定。
装饰器执行顺序
多个装饰器叠加时,求值自上而下,应用自下而上(像洋葱):
@A @B method() {} // 等价于 A(B(method)),B 先生效
四、React 里的对应形态
React 早期用高阶组件(HOC)做装饰,现在更多用 Hooks 和组合:
// HOC:一个接收组件、返回增强组件的函数,就是装饰器
const withLoading = Component => props =>
props.loading ? <Spinner /> : <Component {...props} />;
// 现代写法:自定义 Hook 复用逻辑,避免 HOC 的嵌套地狱和 props 冲突
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let cancelled = false; // 防止组件卸载后 setState
fetch(url).then(r => r.json()).then(d => {
if (!cancelled) { setData(d); setLoading(false); }
});
return () => { cancelled = true; };
}, [url]);
return { data, loading };
}
五、装饰器 vs 代理
两者代码结构几乎一样,区别只在意图:
- 装饰器:我要添加功能,原功能一定会被执行;
- 代理:我要控制访问,原功能可能根本不执行(无权限、命中缓存、还没到时候)。
更多对比见代理模式最后一节。
下一步 👉 策略模式
