装饰器本质是高阶函数,接收目标函数或类并返回新函数或类以插入额外行为;必须显式返回新函数,类方法装饰器需修改descriptor.value,@debounce失效主因是this丢失或timer共享,与Proxy相比装饰器为编译期静态包装且可组合。

装饰器本质是高阶函数
JavaScript 中的装饰器(目前为 Stage 3 提案,需 Babel 或 TypeScript 支持)不是语法糖,而是对目标函数或类进行「包装」的高阶函数。它接收原函数(或类、属性描述符),返回一个新函数(或新类、新描述符),从而在不修改原逻辑的前提下插入额外行为。
常见误解是认为 @log 是“自动加日志”,其实它等价于:fn = log(fn) —— 必须显式返回新函数,否则原函数会被覆盖为 undefined。
- 装饰器函数本身必须是同步的;不能
await或返回 Promise(除非你手动处理异步包装) - 类方法装饰器接收三个参数:
target(原型)、key(方法名)、descriptor(属性描述符),修改descriptor.value才能改行为 - 普通函数装饰器(如用于独立函数)需配合 Babel 的
@babel/plugin-proposal-decorators+legacy: true模式,否则仅支持类/成员
@debounce 装饰器为什么常失效
防抖装饰器写错最典型的症状是:调用后无反应,或防抖完全不生效。根本原因在于没有正确保存和调用原始函数,或闭包中引用了错误的上下文。
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 替代装饰器做函数增强,但二者定位不同:装饰器是编译期/定义期的静态包装,Proxy 是运行时动态拦截。这意味着:
立即学习“Java免费学习笔记(深入)”;
- 装饰器只在类定义或函数声明时执行一次,生成固定包装函数;Proxy 每次调用都触发
apply钩子,开销更高 - 装饰器可组合(
@log @auth @cache),顺序从下到上执行;Proxy 通常单层封装,多层需嵌套new Proxy(new Proxy(...)) - 装饰器对类型系统友好(TypeScript 可推导返回类型);Proxy 返回的类型默认是
any,需手动声明 - 装饰器无法拦截属性读取(如
obj.x),而Proxy可通过get钩子做到
生产环境用装饰器要注意什么
目前(2024)Chrome / Safari / Firefox 均未原生支持装饰器语法,所有实际使用都依赖转译。这带来几个硬性约束:
- Babel 用户必须启用
legacy: true(对应旧版装饰器提案),否则@语法报错;新提案(tc39/proposal-decorators)行为不同,且尚无主流工具链稳定支持 - TypeScript 默认启用的是旧版装饰器,若升级 TS 版本,需检查
tsconfig.json中"experimentalDecorators": true是否仍匹配构建流程 - 装饰器内部不能依赖尚未初始化的模块(如
import()动态导入),因为装饰器在模块加载阶段就执行,此时异步 import 还未完成 - 服务端渲染(SSR)中,若装饰器含浏览器专属 API(如
localStorage),需包裹if (typeof window !== 'undefined')
真正难的不是写出一个 @memoize,而是确保它在热更新、Tree-shaking、跨平台运行时都不意外崩溃。










