JavaScript 装饰器(Decorator)是 ES7 提案中的特性,核心是通过“包装目标对象”,在不修改原对象源码的前提下,动态扩展其功能,本质是“高阶函数的语法糖”,让代码复用、功能增强更简洁优雅,已广泛应用于 React、Vue3、Node.js 等主流技术栈。
一、装饰器核心原理
1. 底层逻辑
装饰器本质是「接收目标对象、返回新对象(或修改原对象)的高阶函数」,核心流程:
- 拦截目标对象(类、方法、属性等)的定义/创建过程;
- 对目标对象进行功能增强(如添加日志、权限校验、缓存等);
- 返回增强后的对象,替换原目标对象生效。
2. 语法本质(去糖示例)
装饰器的@xxx语法是简化写法,底层可手动拆解为高阶函数调用,清晰理解原理:
// 1. 定义装饰器(本质是高阶函数)functionlogDecorator(target,name,descriptor){constoriginFn=descriptor.value;// 保存原方法descriptor.value=function(...args){// 重写方法,添加增强逻辑console.log(`调用方法${name},参数:${args}`);constresult=originFn.apply(this,args);// 执行原方法console.log(`方法${name}执行完成,返回值:${result}`);returnresult;};returndescriptor;// 返回修改后的描述符}// 2. 装饰器语法(@ 糖衣)classUser{@logDecoratoradd(name){return`添加用户:${name}`;}}// 3. 去糖后(等价于手动调用高阶函数)classUserOrigin{add(name){return`添加用户:${name}`;}}// 手动获取方法描述符,调用装饰器,重新定义方法constdescriptor=Object.getOwnPropertyDescriptor(UserOrigin.prototype,'add');constnewDescriptor=logDecorator(UserOrigin.prototype,'add',descriptor);Object.defineProperty(UserOrigin.prototype,'add',newDescriptor);// 调用验证:两种写法效果完全一致newUser().add('张三');// 输出日志 + 返回结果newUserOrigin().add('李四');// 输出日志 + 返回结果3. 核心概念:属性描述符(descriptor)
装饰器操作的核心是「对象属性描述符」,方法/属性的装饰器会接收descriptor参数,其关键属性:
value:目标方法/属性的值(方法装饰器核心操作此属性);writable:是否可修改(默认true);enumerable:是否可枚举(默认false,类方法默认不可遍历);configurable:是否可删除/修改描述符(默认false)。
二、装饰器的分类(含使用场景+实例)
根据装饰目标不同,分为 5 大类,类装饰器、方法装饰器、属性装饰器最常用,优先掌握:
1. 类装饰器(装饰整个类)
作用
- 给类添加静态属性/方法;
- 修改类的构造函数逻辑;
- 扩展类的实例功能(如混入 Mixin)。
语法
装饰器函数仅接收 1 个参数:target(目标类本身),返回值为「新类」或「修改后的原类」。
实战场景 1:给类添加静态属性
// 装饰器:给类添加「版本号」「创建时间」静态属性functionaddStaticInfo(target){target.version='1.0.0';// 静态属性target.createTime=newDate().toLocaleString();// 静态属性target.getInfo=function(){// 静态方法return`版本:${this.version},创建时间:${this.createTime}`;};returntarget;// 返回修改后的类}// 使用装饰器@addStaticInfoclassUserService{getUser(){return{id:1,name:'张三'};}}// 验证效果console.log(UserService.version);// 1.0.0console.log(UserService.getInfo());// 版本:1.0.0,创建时间:xxx实战场景 2:修改类的构造函数(注入默认属性)
// 装饰器:给实例注入默认的「状态属性」functioninjectDefaultState(defaultState){// 装饰器支持传参:外层函数接收参数,内层函数是真正的装饰器returnfunction(target){// 保存原构造函数constOriginClass=target;// 定义新构造函数,注入默认属性functionNewClass(...args){OriginClass.apply(this,args);// 执行原构造逻辑this.state={...defaultState,...this.state};// 合并默认状态}NewClass.prototype=Object.create(OriginClass.prototype);// 继承原型NewClass.prototype.constructor=NewClass;returnNewClass;// 返回新类替换原类};}// 使用装饰器(传入默认状态参数)@injectDefaultState({status:'active',role:'user'})classUser{constructor(name){this.name=name;// 实例可自定义 state,会覆盖默认值this.state={role:'admin'};}}// 验证效果:state 合并默认值,自定义属性覆盖默认值constuser=newUser('李四');console.log(user.state);// { status: 'active', role: 'admin' }2. 方法装饰器(装饰类的原型方法/静态方法)
作用
- 日志打印(调用前/后记录参数、返回值);
- 权限校验(执行方法前判断权限);
- 缓存优化(缓存方法返回结果,避免重复计算);
- 异常捕获(统一捕获方法执行错误)。
语法
装饰器函数接收 3 个参数:
target:原型方法 → 类的原型;静态方法 → 类本身;name:目标方法的名称;descriptor:目标方法的属性描述符(核心操作value)。
实战场景 1:日志打印(最常用)
// 装饰器:记录方法调用日志(支持传参:是否打印返回值)functionmethodLog(showResult=true){returnfunction(target,name,descriptor){constoriginFn=descriptor.value;descriptor.value=asyncfunction(...args){// 调用前日志conststartTime=Date.now();console.log(`[${newDate().toISOString()}] 方法${name}开始调用,参数:`,args);letresult;try{result=awaitoriginFn.apply(this,args);// 支持异步方法// 调用成功日志if(showResult)console.log(`[${newDate().toISOString()}] 方法${name}调用成功,返回值:`,result);console.log(`[${newDate().toISOString()}] 方法${name}执行耗时:${Date.now()-startTime}ms`);}catch(err){// 调用失败日志console.error(`[${newDate().toISOString()}] 方法${name}调用失败,错误:`,err);throwerr;// 抛出错误,不阻断业务逻辑}returnresult;};returndescriptor;};}classOrderService{// 装饰原型方法(异步),传参:显示返回值@methodLog(true)asynccreateOrder(price,goods){awaitnewPromise(resolve=>setTimeout(resolve,100));// 模拟接口请求return{orderId:Date.now(),price,goods};}// 装饰静态方法,传参:不显示返回值@methodLog(false)staticcancelOrder(orderId){if(!orderId)thrownewError('订单ID不能为空');returntrue;}}// 验证效果newOrderService().createOrder(99,['手机']);// 打印完整日志(含返回值+耗时)OrderService.cancelOrder('');// 打印错误日志,抛出异常实战场景 2:缓存优化(避免重复计算)
// 装饰器:缓存方法返回值(key 为参数拼接,支持基本类型参数)functioncacheDecorator(){returnfunction(target,name,descriptor){constoriginFn=descriptor.value;constcache=newMap();// 缓存容器:key=参数字符串,value=返回值descriptor.value=function(...args){constcacheKey=JSON.stringify(args);// 参数转字符串作为 key// 命中缓存,直接返回if(cache.has(cacheKey)){console.log(`方法${name}命中缓存,参数:`,args);returncache.get(cacheKey);}// 未命中缓存,执行原方法,存入缓存constresult=originFn.apply(this,args);cache.set(cacheKey,result);returnresult;};returndescriptor;};}classCalcService{// 装饰计算方法(高耗时场景:如大数据排序、复杂公式计算)@cacheDecorator()sum(a,b){console.log(`执行 sum 计算:${a}+${b}`);returna+b;}}constcalc=newCalcService();calc.sum(10,20);// 未命中,执行计算,输出日志 → 30calc.sum(10,20);// 命中缓存,直接返回,输出缓存日志 → 30calc.sum(30,40);// 未命中,执行计算 → 703. 属性装饰器(装饰类的原型属性/静态属性)
作用
- 限制属性的取值/赋值规则(如类型校验、范围限制);
- 给属性设置默认值;
- 监听属性变化(类似 Vue 的 watch)。
语法
装饰器函数接收 2 个参数(无descriptor,需手动获取/修改):
target:原型属性 → 类的原型;静态属性 → 类本身;name:目标属性的名称。
实战场景:属性类型校验(防止赋值错误)
// 装饰器:校验属性类型(接收允许的类型数组,如 [String, Number])functionvalidateType(allowTypes){returnfunction(target,name){letvalue;// 存储属性实际值// 重新定义属性,通过 get/set 实现类型校验Object.defineProperty(target,name,{get(){returnvalue;// 取值时返回存储的值},set(newVal){// 校验新值类型是否在允许范围内constisLegal=allowTypes.some(type=>newValinstanceoftype);if(isLegal){value=newVal;}else{consttypeNames=allowTypes.map(t=>t.name).join('/');console.error(`属性${name}类型错误,允许类型:${typeNames},当前类型:${newVal?.constructor?.name}`);}},enumerable:true,// 允许遍历属性configurable:true});};}classUser{// 原型属性:仅允许 String 类型@validateType([String])name;// 静态属性:仅允许 Number 类型@validateType([Number])staticageLimit;}// 验证效果constuser=newUser();user.name='张三';// 合法,赋值成功user.name=123;// 非法,打印错误,不赋值console.log(user.name);// 张三User.ageLimit=18;// 合法,赋值成功User.ageLimit='18';// 非法,打印错误,不赋值console.log(User.ageLimit);// 184. 访问器装饰器(装饰类的 get/set 方法)
作用
- 增强 get 方法(如返回值格式化);
- 增强 set 方法(如赋值前数据清洗、权限校验)。
语法
与方法装饰器一致,接收target、name、descriptor,descriptor含get(取值函数)和set(赋值函数)属性。
实战场景:属性赋值清洗(去除字符串空格)
// 装饰器:清洗字符串属性(去除首尾空格,空字符串转为 null)functiontrimString(target,name,descriptor){constoriginSet=descriptor.set;// 保存原 set 方法// 重写 set 方法,添加清洗逻辑descriptor.set=function(newVal){letcleanVal=newVal;if(typeofnewVal==='string'){cleanVal=newVal.trim();// 去除首尾空格if(cleanVal==='')cleanVal=null;// 空字符串转 null}originSet.call(this,cleanVal);// 执行原 set 方法};returndescriptor;}classProduct{constructor(){this._title='';// 私有属性(约定俗成)}// 装饰访问器 set 方法@trimStringsettitle(val){this._title=val;}gettitle(){returnthis._title||'无标题';}}// 验证效果constproduct=newProduct();product.title=' 手机 ';// 赋值带空格的字符串console.log(product.title);// 手机(空格被去除)product.title=' ';// 赋值全空格字符串console.log(product.title);// 无标题(空字符串转 null,get 方法返回默认值)5. 参数装饰器(装饰方法的参数)
作用
- 标记参数(如标记“必填参数”);
- 校验参数合法性(如参数非空、范围校验)。
语法
装饰器函数接收 3 个参数:
target:原型方法 → 类的原型;静态方法 → 类本身;name:目标方法的名称;index:当前参数在方法参数列表中的索引(从 0 开始)。
实战场景:标记必填参数(校验参数非空)
// 1. 存储必填参数的容器(key:方法名,value:必填参数索引数组)constrequiredParams=newMap();// 2. 参数装饰器:标记参数为必填functionrequired(target,name,index){if(!requiredParams.has(name)){requiredParams.set(name,[]);}requiredParams.get(name).push(index);// 记录必填参数的索引}// 3. 方法装饰器:校验必填参数(需配合参数装饰器使用)functioncheckRequired(target,name,descriptor){constoriginFn=descriptor.value;descriptor.value=function(...args){// 获取当前方法的必填参数索引constrequiredIndexes=requiredParams.get(name)||[];// 校验每个必填参数是否为空for(constindexofrequiredIndexes){constparam=args[index];if(param===undefined||param===null||param===''){thrownewError(`方法${name}的第${index+1}个参数为必填项,不可为空`);}}returnoriginFn.apply(this,args);};returndescriptor;}classLoginService{// 方法装饰器(校验必填)+ 参数装饰器(标记必填)@checkRequiredlogin(@required username,@required password,rememberMe=false){console.log(`登录:用户名=${username},记住密码=${rememberMe}`);returntrue;}}// 验证效果constloginService=newLoginService();loginService.login('admin','123456');// 合法,执行成功loginService.login('','123456');// 非法,第 1 个参数为空,抛出错误loginService.login('admin',null);// 非法,第 2 个参数为空,抛出错误三、装饰器的核心作用
- 解耦功能增强逻辑:将日志、权限、缓存等通用功能与业务逻辑分离,避免代码冗余(如每个方法都写一遍日志);
- 代码复用性极高:通用装饰器(如日志、缓存)可在全项目多个类/方法中复用,减少重复开发;
- 动态扩展功能:无需修改原代码,通过添加/删除装饰器,快速开启/关闭功能(如测试环境加日志,生产环境移除);
- 代码可读性提升:
@xxx语法直观,一眼能看出目标对象的增强逻辑(如@checkRequired即知方法有必填校验); - 符合开闭原则:对扩展开放(新增装饰器增强功能),对修改关闭(不改动原业务代码)。
四、装饰器的设计思路(落地核心)
1. 单一职责原则
一个装饰器只做一件事(如logDecorator只处理日志,cacheDecorator只处理缓存),避免装饰器逻辑臃肿,便于复用和维护。
2. 支持参数配置
装饰器通过“外层函数接收参数,内层函数实现逻辑”,适配不同场景(如@methodLog(true)显示返回值,@methodLog(false)不显示)。
3. 兼容异步方法
通过async/await处理异步方法,确保增强逻辑(如日志、异常捕获)对同步/异步方法都生效(参考方法装饰器的日志示例)。
4. 不破坏原逻辑
装饰器需先保存原对象(原类、原方法),增强后通过apply/call执行原逻辑,避免覆盖原功能(核心:“包装”而非“替换”)。
5. 可组合使用
多个装饰器可叠加在同一目标上,执行顺序为「从上到下定义,从下到上执行」(类似洋葱模型):
// 定义 3 个简单装饰器functiondecoratorA(target,name,descriptor){console.log('执行装饰器 A');returndescriptor;}functiondecoratorB(target,name,descriptor){console.log('执行装饰器 B');returndescriptor;}functiondecoratorC(target,name,descriptor){console.log('执行装饰器 C');returndescriptor;}classTest{// 装饰器顺序:A → B → C(定义顺序),执行顺序:C → B → A@decoratorA @decoratorB @decoratorCfn(){}}newTest().fn();// 输出:执行装饰器 C → 执行装饰器 B → 执行装饰器 A五、实际项目中的应用方案(前端+Node.js)
1. 前端项目(Vue3/React)
场景 1:Vue3 组件装饰器(配合vue-class-component)
Vue3 支持通过装饰器简化组件逻辑(需安装依赖vue-class-component):
<template> <div>{{ username }} - {{ role }}</div> </template> <script lang="ts"> import { Component, Vue, Prop, Watch } from 'vue-class-component'; // 自定义装饰器:给组件添加权限判断逻辑 function checkRole(allowRoles: string[]) { return function(target: Vue, name: string, descriptor: PropertyDescriptor) { const originFn = descriptor.value; descriptor.value = function(...args) { const role = this.role; // 组件实例的 role 属性 if (allowRoles.includes(role)) { originFn.apply(this, args); } else { alert('无权限执行此操作'); } }; return descriptor; }; } @Component export default class UserComponent extends Vue { // 属性装饰器:定义 props(类型校验+默认值) @Prop({ type: String, default: '游客' }) username!: string; role = 'user'; // 组件实例属性 // 访问器装饰器:监听属性变化(类似 Vue 的 watch) @Watch('username') onUsernameChange(newVal: string) { console.log('用户名变化:', newVal); } // 方法装饰器:权限校验 @checkRole(['admin', 'editor']) editUser() { console.log('编辑用户'); } } </script>场景 2:React 组件装饰器(高阶组件语法糖)
React 的高阶组件(HOC)可通过装饰器简化写法(如withRouter、connect):
import React from 'react'; import { withRouter, RouteComponentProps } from 'react-router-dom'; import { connect } from 'react-redux'; // 自定义装饰器:给组件添加加载状态 function withLoading(Component: React.ComponentType) { return function LoadingComponent(props: any) { if (props.loading) return <div>加载中...</div>; return <Component {...props} />; }; } // 装饰器组合使用:路由注入 + Redux 状态注入 + 加载状态 @withRouter @connect((state) => ({ user: state.user, loading: state.loading })) @withLoading class Home extends React.Component<RouteComponentProps> { render() { return <div>首页 - 用户名:{this.props.user.name}</div>; } } export default Home;2. Node.js 项目(接口层/服务层)
场景 1:接口请求日志(Express/Koa 中间件装饰器)
给接口方法添加日志,记录请求参数、响应结果、耗时:
// 装饰器:接口请求日志functionapiLog(target,name,descriptor){constoriginFn=descriptor.value;descriptor.value=asyncfunction(req,res,next){conststartTime=Date.now();console.log(`[API请求] 路径:${req.path},方法:${req.method},参数:`,req.body);try{constresult=awaitoriginFn.apply(this,[req,res,next]);// 执行接口逻辑res.json({code:200,data:result});// 统一响应格式console.log(`[API响应] 路径:${req.path},耗时:${Date.now()-startTime}ms,结果:`,result);}catch(err){res.json({code:500,msg:err.message});// 统一错误响应console.error(`[API错误] 路径:${req.path},错误:`,err);next(err);}};returndescriptor;}// 服务层:用户接口classUserController{// 装饰接口方法@apiLogasyncgetUserList(req){const{page=1,size=10}=req.query;// 业务逻辑:查询数据库return{list:[{id:1,name:'张三'}],total:1};}@apiLogasynccreateUser(req){const{name,age}=req.body;if(!name)thrownewError('用户名必填');// 业务逻辑:插入数据库return{id:Date.now(),name,age};}}// Express 路由注册constexpress=require('express');constapp=express();constuserController=newUserController();app.use(express.json());app.get('/api/users',userController.getUserList);app.post('/api/users',userController.createUser);app.listen(3000);场景 2:数据库操作缓存(Redis 装饰器)
给数据库查询方法添加 Redis 缓存,减少数据库压力:
constredis=require('redis');constclient=redis.createClient({url:'redis://localhost:6379'});client.connect();// 装饰器:Redis 缓存(key 前缀+参数拼接,过期时间 5 分钟)functionredisCache(prefix='cache:',expire=300){returnasyncfunction(target,name,descriptor){constoriginFn=descriptor.value;descriptor.value=asyncfunction(...args){constcacheKey=`${prefix}${name}:${JSON.stringify(args)}`;// 先查 Redis 缓存constcacheData=awaitclient.get(cacheKey);if(cacheData){console.log(`Redis 缓存命中,key:${cacheKey}`);returnJSON.parse(cacheData);}// 缓存未命中,查数据库constdata=awaitoriginFn.apply(this,args);// 存入 Redis,设置过期时间awaitclient.setEx(cacheKey,expire,JSON.stringify(data));console.log(`Redis 缓存存入,key:${cacheKey},过期时间:${expire}s`);returndata;};returndescriptor;};}// 数据层:用户数据库操作classUserDao{// 装饰查询方法,添加 Redis 缓存@redisCache('user:')asyncselectUserById(id){console.log(`查询数据库:用户ID=${id}`);// 模拟数据库查询return{id,name:'张三',age:20};}}// 验证效果constuserDao=newUserDao();userDao.selectUserById(1);// 查数据库,存入缓存userDao.selectUserById(1);// 查 Redis 缓存,不查数据库六、浏览器兼容性方案
装饰器是 ES7 提案,原生浏览器不支持(仅部分版本 Chrome 开启实验性标志支持),需通过工具编译为 ES5/ES6 代码,主流方案如下:
1. 核心编译工具:Babel
步骤 1:安装依赖
# 核心依赖:Babel 预设 + 装饰器插件npminstall@babel/core @babel/cli @babel/preset-env @babel/plugin-proposal-decorators @babel/plugin-proposal-class-properties --save-dev步骤 2:配置 Babel(.babelrc 或 babel.config.json)
{"presets":[["@babel/preset-env",{"targets":"> 0.25%, not dead",// 适配主流浏览器"useBuiltIns":"usage",// 自动引入 polyfill"corejs":3// core-js 版本(处理 ES 新特性兼容)}],],"plugins":[// 装饰器插件:必须放在 class-properties 前面["@babel/plugin-proposal-decorators",{"version":"legacy"}],// legacy 兼容旧语法["@babel/plugin-proposal-class-properties",{"loose":true}]// 支持类属性直接定义]}步骤 3:编译代码
# package.json 添加脚本"scripts":{"build":"babel src --out-dir dist"// 将 src 目录代码编译到 dist 目录}# 执行编译npmrun build2. 工程化项目集成(Vue3/Vite/React)
场景 1:Vue3 + Vite(无需额外配置 Babel)
Vite 内置@vitejs/plugin-vue-jsx插件,支持装饰器(需配置legacy: true):
// vite.config.jsimport{defineConfig}from'vite';importvuefrom'@vitejs/plugin-vue';importvueJsxfrom'@vitejs/plugin-vue-jsx';exportdefaultdefineConfig({plugins:[vue(),vueJsx({// 启用装饰器支持plugins:[['@babel/plugin-proposal-decorators',{version:'legacy'}],['@babel/plugin-proposal-class-properties',{loose:true}]]})]});场景 2:React + Webpack(Create React App 配置)
// webpack.config.jsmodule.exports={module:{rules:[{test:/\.(js|jsx|ts|tsx)$/,exclude:/node_modules/,use:{loader:'babel-loader',options:{presets:['@babel/preset-react','@babel/preset-typescript'],plugins:[['@babel/plugin-proposal-decorators',{version:'legacy'}],['@babel/plugin-proposal-class-properties',{loose:true}]]}}}]}};3. TypeScript 项目支持
TS 原生支持装饰器,只需在tsconfig.json中开启配置:
{"compilerOptions":{"target":"ES6",// 目标版本"experimentalDecorators":true,// 开启装饰器(关键)"emitDecoratorMetadata":true,// 生成装饰器元数据(可选,如需要反射时开启)"module":"ESNext","outDir":"./dist"},"include":["src/**/*"]}4. 兼容性注意事项
- 装饰器提案仍在迭代,
legacy模式是目前最稳定的兼容方案,避免使用2023-05等实验性版本; - 低版本浏览器(如 IE11)需配合
core-js@3引入 polyfill,处理Map、Promise等新特性; - 避免在生产环境使用未编译的装饰器语法,会导致浏览器报错。
总结
JavaScript 装饰器是“高阶函数的优雅封装”,核心价值是「解耦、复用、动态扩展」,重点掌握类装饰器、方法装饰器、属性装饰器的使用,结合 Babel/TS 解决兼容性问题,可在前端组件、接口日志、权限校验、缓存优化等场景大幅提升开发效率。实际项目中,建议封装通用装饰器库(如日志、缓存、权限),统一团队使用规范,减少重复开发。
补充:装饰器进阶实战与避坑指南
一、通用装饰器工具库封装(可直接落地)
基于高频场景,封装 4 个通用装饰器,支持多项目复用,含完整注释与使用示例,适配前端/Vue3/React/Node.js全场景:
1. 日志装饰器(支持同步/异步、自定义前缀)
/** * 方法日志装饰器:记录调用参数、返回值、耗时、错误信息 * @param options 配置项 { prefix: 日志前缀, showResult: 是否显示返回值, showTime: 是否显示耗时 } */exportfunctionlog(options={}){const{prefix='[LOG]',showResult=true,showTime=true}=options;returnfunction(target,methodName,descriptor){constoriginFn=descriptor.value;// 适配同步/异步方法descriptor.value=asyncfunction(...args){conststartTime=Date.now();// 调用前日志console.log(`${prefix}方法${methodName}开始调用,参数:`,args);try{constresult=awaitoriginFn.apply(this,args);// 调用成功日志if(showTime)console.log(`${prefix}方法${methodName}执行耗时:${Date.now()-startTime}ms`);if(showResult)console.log(`${prefix}方法${methodName}调用成功,返回值:`,result);returnresult;}catch(error){// 调用失败日志console.error(`${prefix}方法${methodName}调用失败,错误:`,error);throwerror;// 抛出错误,不阻断业务}};returndescriptor;};}2. 缓存装饰器(支持过期时间、手动清除)
/** * 方法缓存装饰器:缓存返回值,避免重复计算/请求 * @param options 配置项 { expire: 过期时间(ms, 0=永久), cacheKeyFn: 自定义缓存key生成函数 } */exportfunctioncache(options={}){const{expire=0,cacheKeyFn}=options;constcacheMap=newMap();// 缓存容器:key=缓存key,value={ data: 数据, expireTime: 过期时间 }// 清除指定方法的缓存(静态方法,需手动调用)cache.clear=function(methodName,...args){constkey=cacheKeyFn?cacheKeyFn(args):JSON.stringify(args);constfullKey=`${methodName}_${key}`;cacheMap.delete(fullKey);console.log(`[CACHE] 清除方法${methodName}的缓存,key:${fullKey}`);};returnfunction(target,methodName,descriptor){constoriginFn=descriptor.value;descriptor.value=asyncfunction(...args){// 生成缓存key(支持自定义生成逻辑)constkey=cacheKeyFn?cacheKeyFn(args):JSON.stringify(args);constfullKey=`${methodName}_${key}`;constcacheItem=cacheMap.get(fullKey);// 缓存命中且未过期,直接返回if(cacheItem){const{data,expireTime}=cacheItem;if(expire===0||Date.now()<expireTime){console.log(`[CACHE] 方法${methodName}命中缓存,key:${fullKey}`);returndata;}// 缓存过期,删除旧缓存cacheMap.delete(fullKey);}// 缓存未命中,执行原方法constdata=awaitoriginFn.apply(this,args);// 存入缓存(计算过期时间)constexpireTime=expire>0?Date.now()+expire:Infinity;cacheMap.set(fullKey,{data,expireTime});console.log(`[CACHE] 方法${methodName}缓存存入,key:${fullKey},过期时间:${expire||'永久'}ms`);returndata;};returndescriptor;};}3. 权限校验装饰器(支持角色/权限码校验)
/** * 权限校验装饰器:执行方法前校验权限,无权限则抛出错误/执行回调 * @param options 配置项 { allowRoles: 允许的角色数组, allowPerms: 允许的权限码数组, noAuthCb: 无权限回调 } */exportfunctionauth(options={}){const{allowRoles=[],allowPerms=[],noAuthCb}=options;if(allowRoles.length===0&&allowPerms.length===0){thrownewError('[AUTH] 权限装饰器需配置 allowRoles 或 allowPerms');}returnfunction(target,methodName,descriptor){constoriginFn=descriptor.value;descriptor.value=function(...args){// 假设从全局获取当前用户权限(实际项目需从Vuex/Redux/全局状态获取)constcurrentUser={role:'user',perms:['user:view']};// 示例数据consthasRole=allowRoles.length===0||allowRoles.includes(currentUser.role);consthasPerm=allowPerms.length===0||allowPerms.some(perm=>currentUser.perms.includes(perm));// 有权限:执行原方法;无权限:执行回调/抛错if(hasRole&&hasPerm){returnoriginFn.apply(this,args);}else{if(typeofnoAuthCb==='function'){noAuthCb(methodName,currentUser);return;}thrownewError(`[AUTH] 无权限执行方法${methodName},当前角色:${currentUser.role},权限:${currentUser.perms.join(',')}`);}};returndescriptor;};}4. 必填参数校验装饰器(支持多参数标记)
/** * 标记参数为必填(配合 requiredCheck 方法装饰器使用) */exportfunctionrequired(target,methodName,paramIndex){// 存储必填参数:key=方法名,value=必填参数索引数组if(!target.__requiredParams__){target.__requiredParams__=newMap();}if(!target.__requiredParams__.has(methodName)){target.__requiredParams__.set(methodName,[]);}target.__requiredParams__.get(methodName).push(paramIndex);}/** * 必填参数校验装饰器:校验标记为 @required 的参数是否为空 */exportfunctionrequiredCheck(target,methodName,descriptor){constoriginFn=descriptor.value;constrequiredParams=target.__requiredParams__?.get(methodName)||[];descriptor.value=function(...args){for(constindexofrequiredParams){constparam=args[index];// 判定为空:undefined/null/空字符串/空数组(可根据需求调整)constisEmpty=param===undefined||param===null||param===''||(Array.isArray(param)&¶m.length===0);if(isEmpty){thrownewError(`[PARAM] 方法${methodName}的第${index+1}个参数为必填项,不可为空`);}}returnoriginFn.apply(this,args);};returndescriptor;}工具库使用示例(Vue3 组件实战)
<template> <button @click="getUserList(1, 10)">获取用户列表</button> <button @click="editUser(1, '张三')">编辑用户</button> </template> <script setup lang="ts"> import { log, cache, auth, required, requiredCheck } from '@/utils/decorators'; class UserService { // 1. 日志+缓存:缓存5分钟,显示耗时,不显示返回值 @log({ prefix: '[USER-API]', showResult: false, showTime: true }) @cache({ expire: 5 * 60 * 1000 }) async getUserList(page: number, size: number) { // 模拟接口请求 await new Promise(resolve => setTimeout(resolve, 200)); return { list: [{ id: 1, name: '张三' }], total: 100 }; } // 2. 权限校验+必填参数:仅admin角色可执行,userName为必填 @auth({ allowRoles: ['admin'], noAuthCb: (fnName) => alert(`无权限执行 ${fnName} 操作`) }) @requiredCheck editUser(userId: number, @required userName: string) { console.log(`编辑用户:${userId} - ${userName}`); } } const userService = new UserService(); // 调用方法,自动触发装饰器逻辑 const getUserList = (page: number, size: number) => userService.getUserList(page, size); const editUser = (userId: number, userName: string) => userService.editUser(userId, userName); </script>二、装饰器在框架中的深度应用
1. Vue3 + Pinia 状态管理(装饰器简化模块)
配合pinia-class-decorator库,用装饰器定义 Pinia 模块,简化语法:
// 安装依赖:npm install pinia-class-decoratorimport{defineStore}from'pinia';import{Store,State,Action,Getter}from'pinia-class-decorator';import{log,cache}from'@/utils/decorators';// 装饰器定义 Pinia 模块@StoreexportdefaultclassUserStoreextendsStore{// 状态(等价于 state: () => ({ ... }))@State()userInfo={id:'',name:'',role:'user'};@State()token='';// 计算属性(等价于 getters: { ... })@Getter()getisAdmin(){returnthis.userInfo.role==='admin';}// 动作(等价于 actions: { ... })@Action()setToken(newToken:string){this.token=newToken;localStorage.setItem('token',newToken);}// 异步动作 + 日志装饰器@Action()@log({prefix:'[PINIA-ACTION]'})@cache({expire:30*60*1000})// 缓存用户信息30分钟asyncfetchUserInfo(){// 模拟接口请求constres=awaitfetch('/api/user/info').then(res=>res.json());this.userInfo=res.data;returnres.data;}}2. Node.js 接口分层(装饰器统一处理跨域/限流)
在 Node.js 接口层用装饰器统一处理通用逻辑,简化中间件配置:
// 1. 限流装饰器:限制接口请求频率(如10次/分钟)exportfunctionrateLimit(options={max:10,windowMs:60*1000}){const{max,windowMs}=options;constrequestMap=newMap();// key=IP,value={ count: 请求次数, lastTime: 最后请求时间 }returnfunction(target,methodName,descriptor){constoriginFn=descriptor.value;descriptor.value=asyncfunction(req,res,next){constclientIp=req.ip;// 获取客户端IPconstnow=Date.now();constrequestInfo=requestMap.get(clientIp)||{count:0,lastTime:now};// 超出时间窗口,重置请求次数if(now-requestInfo.lastTime>windowMs){requestInfo.count=1;requestInfo.lastTime=now;}else{requestInfo.count++;// 超出请求限制,返回429if(requestInfo.count>max){returnres.status(429).json({code:429,msg:'请求过于频繁,请稍后再试'});}}requestMap.set(clientIp,requestInfo);returnoriginFn.apply(this,[req,res,next]);};returndescriptor;};}// 2. 接口使用装饰器(跨域+限流+日志)classOrderController{@cors()// 自定义跨域装饰器(简化cors中间件)@rateLimit({max:5,windowMs:30*1000})// 5次/30秒@log({prefix:'[ORDER-API]'})asyncgetOrderList(req,res){constorders=awaitOrderModel.find({userId:req.query.userId});res.json({code:200,data:orders});}}三、装饰器避坑指南(高频问题+解决方案)
1. 装饰器执行顺序错误
- 问题:多个装饰器叠加时,逻辑执行不符合预期;
- 原理:装饰器执行顺序为「从上到下定义,从下到上执行」(洋葱模型);
- 示例:
@A @B @C fn()→ 执行顺序:C → B → A; - 解决方案:按「核心逻辑在下,辅助逻辑在上」的顺序定义(如先缓存,再日志,最后权限)。
2. 异步方法装饰器未处理 Promise
- 问题:异步方法的返回值/错误无法被装饰器捕获;
- 原因:装饰器未用
async/await处理原方法的 Promise; - 解决方案:装饰器内用
async function包裹,await originFn.apply(this, args)(参考通用日志装饰器)。
3. 缓存装饰器参数为引用类型(key 失效)
- 问题:参数为对象/数组时,
JSON.stringify(args)可能生成相同 key(如不同引用的空对象); - 解决方案:提供
cacheKeyFn自定义 key 生成逻辑,或用lodash.isEqual深比较参数;
// 自定义缓存key(处理引用类型)@cache({cacheKeyFn:(args)=>{const[user,page]=args;return`${user.id}_${page}`;// 用对象的唯一标识生成key}})asyncgetUserOrders(user,page){}4. TypeScript 装饰器提示“experimentalDecorators”警告
- 问题:TS 项目中使用装饰器,编辑器提示实验性特性警告;
- 解决方案:在
tsconfig.json中开启experimentalDecorators: true和emitDecoratorMetadata: true(参考前文兼容性配置)。
5. 装饰器修改原对象导致副作用
- 问题:装饰器直接修改原方法/属性,导致其他地方使用原对象时逻辑异常;
- 原理:装饰器应“包装”原对象,而非直接修改;
- 解决方案:先保存原对象(
const originFn = descriptor.value),增强后通过apply/call执行,不直接覆盖原对象。
四、装饰器未来趋势(ES 标准进展)
目前装饰器处于ES2023 提案阶段(Stage 3),与早期legacy模式相比,有 3 个核心变化:
- 装饰器返回值:新标准装饰器返回「新对象」替换原对象,而非修改
descriptor; - 类装饰器支持参数:无需外层函数包裹,可直接给类装饰器传参;
- 私有属性装饰:支持装饰类的私有属性(
#privateProp)。
新标准装饰器示例(未来语法)
// 类装饰器直接传参(新标准)@injectDefaultState({status:'active'})classUser{}// 方法装饰器返回新方法(新标准)functionlog(target,methodName,{get,set}){return{get(){constfn=get.call(this);returnfunction(...args){console.log('调用方法:',methodName);returnfn.apply(this,args);};}};}兼容建议:目前生产环境仍优先使用legacy模式(Babel/TS 成熟支持),待新标准稳定后,可通过工具自动迁移语法。
最终总结
装饰器的核心是「无侵入式增强」,通过封装通用逻辑,实现代码的解耦与复用,是前端/Node.js 项目中提升开发效率的关键技巧。掌握「类/方法/属性」三大核心装饰器,结合通用工具库封装,可快速落地到 Vue3/React/Node.js 项目中,解决日志、缓存、权限等高频场景问题。
实际开发中,需注意装饰器执行顺序、异步处理、兼容性配置,避开常见坑点,同时关注 ES 标准进展,逐步适配新语法。建议团队内部统一装饰器规范,沉淀通用工具库,最大化发挥装饰器的价值。