装饰器本质是高阶函数,接收目标函数或类并返回新函数或类以插入额外行为;必须显式返回新函数,类方法装饰器需修改descriptor.value,@debounce失效主因是this丢失或timer共享,与Proxy相比装饰器为编译期静态包装且可组合。
JavaScript 中的装饰器(目前为 Stage 3 提案,需 Babel 或 TypeScript 支持)不是语法糖,而是对目标函数或类进行「包装」的高阶函数。它接收原函数(或类、属性描述符),返回一个新函数(或新类、新描述符),从而在不修改原逻辑的前提下插入额外行为。
常见误解是认为 @log 是“自动加日志”,其实它等价于:fn = log(fn) —— 必须显式返回新函数,否则原函数会被覆盖为 undefined。
await 或返回 Promise(除非你手动处理异步包装)target(原型)、key(方法名)、descriptor(属性描述符),修改 descriptor.value 才能改行为@babel/plugin-proposal-decorators + legacy: true 模式,否则仅支持类/成员防抖装饰器写错最典型的症状是:调用后无反应,或防抖完全不生效。根本原因在于没有正确保存和调用原始函数,或闭包中引用了错误的上下文。
function debounce(wait) {
return function(target, key, descriptor) {
const original = descriptor.value;
let timeoutId = null;
descriptor.value = function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
// 注意:这里必须用 call 绑定 this,否则 original 内部 this 指向丢失
original.call(this, ...args);
}, wait);
};
return descriptor;
};
}
original.call(this, ...args) → this 指向 window 或 undefined(严格模式),导致访问实例属性失败timeoutId 声明在装饰器外层(而非闭包内)→ 所有被装饰方法共享同一个 timer,互相干扰this 和原型链有人试图用 Proxy 替代装饰器做函数增强,但二者定位不同:装饰器是编译期/定义期的静态包装,Proxy 是运行时动态拦截。这意味着:
apply 钩子,开销更高@log @auth @cache),顺序从下到上执行;Proxy 通常单层封装,多层需嵌套 new Proxy(new Proxy(...))
any,需手动声明obj.x),而 Proxy 可通过 get 钩子做到目前(2025)Chrome / S
afari / Firefox 均未原生支持装饰器语法,所有实际使用都依赖转译。这带来几个硬性约束:
legacy: true(对应旧版装饰器提案),否则 @ 语法报错;新提案(tc39/proposal-decorators)行为不同,且尚无主流工具链稳定支持tsconfig.json 中 "experimentalDecorators": true 是否仍匹配构建流程import() 动态导入),因为装饰器在模块加载阶段就执行,此时异步 import 还未完成localStorage),需包裹 if (typeof window !== 'undefined')
真正难的不是写出一个 @memoize,而是确保它在热更新、Tree-shaking、跨平台运行时都不意外崩溃。