JavaScript 装饰器(Decorator)是 ES7 提案中的特性,核心是通过"包装目标对象",在不修改原对象源码的前提下,动态扩展其功能,本质是"高阶函数的语法糖",让代码复用、功能增强更简洁优雅,已广泛应用于 React、Vue3、Node.js 等主流技术栈。
一、装饰器核心原理
1. 底层逻辑
装饰器本质是「接收目标对象、返回新对象(或修改原对象)的高阶函数」,核心流程:
- 拦截目标对象(类、方法、属性等)的定义/创建过程;
- 对目标对象进行功能增强(如添加日志、权限校验、缓存等);
- 返回增强后的对象,替换原目标对象生效。
2. 语法本质(去糖示例)
装饰器的 @xxx 语法是简化写法,底层可手动拆解为高阶函数调用,清晰理解原理:
js
// 1. 定义装饰器(本质是高阶函数)
function logDecorator(target, name, descriptor) {
const originFn = descriptor.value; // 保存原方法
descriptor.value = function(...args) { // 重写方法,添加增强逻辑
console.log(`调用方法 ${name},参数:${args}`);
const result = originFn.apply(this, args); // 执行原方法
console.log(`方法 ${name} 执行完成,返回值:${result}`);
return result;
};
return descriptor; // 返回修改后的描述符
}
// 2. 装饰器语法(@ 糖衣)
class User {
@logDecorator
add(name) {
return `添加用户:${name}`;
}
}
// 3. 去糖后(等价于手动调用高阶函数)
class UserOrigin {
add(name) {
return `添加用户:${name}`;
}
}
// 手动获取方法描述符,调用装饰器,重新定义方法
const descriptor = Object.getOwnPropertyDescriptor(UserOrigin.prototype, 'add');
const newDescriptor = logDecorator(UserOrigin.prototype, 'add', descriptor);
Object.defineProperty(UserOrigin.prototype, 'add', newDescriptor);
// 调用验证:两种写法效果完全一致
new User().add('张三'); // 输出日志 + 返回结果
new UserOrigin().add('李四'); // 输出日志 + 返回结果
3. 核心概念:属性描述符(descriptor)
装饰器操作的核心是「对象属性描述符」,方法/属性的装饰器会接收 descriptor 参数,其关键属性:
value:目标方法/属性的值(方法装饰器核心操作此属性);writable:是否可修改(默认true);enumerable:是否可枚举(默认false,类方法默认不可遍历);configurable:是否可删除/修改描述符(默认false)。
二、装饰器的分类(含使用场景+实例)
根据装饰目标不同,分为 5 大类,类装饰器、方法装饰器、属性装饰器 最常用,优先掌握:
1. 类装饰器(装饰整个类)
作用
- 给类添加静态属性/方法;
- 修改类的构造函数逻辑;
- 扩展类的实例功能(如混入 Mixin)。
语法
装饰器函数仅接收 1 个参数:target(目标类本身),返回值为「新类」或「修改后的原类」。
实战场景 1:给类添加静态属性
js
// 装饰器:给类添加「版本号」「创建时间」静态属性
function addStaticInfo(target) {
target.version = '1.0.0'; // 静态属性
target.createTime = new Date().toLocaleString(); // 静态属性
target.getInfo = function() { // 静态方法
return `版本:${this.version},创建时间:${this.createTime}`;
};
return target; // 返回修改后的类
}
// 使用装饰器
@addStaticInfo
class UserService {
getUser() {
return { id: 1, name: '张三' };
}
}
// 验证效果
console.log(UserService.version); // 1.0.0
console.log(UserService.getInfo()); // 版本:1.0.0,创建时间:xxx
实战场景 2:修改类的构造函数(注入默认属性)
js
// 装饰器:给实例注入默认的「状态属性」
function injectDefaultState(defaultState) {
// 装饰器支持传参:外层函数接收参数,内层函数是真正的装饰器
return function(target) {
// 保存原构造函数
const OriginClass = target;
// 定义新构造函数,注入默认属性
function NewClass(...args) {
OriginClass.apply(this, args); // 执行原构造逻辑
this.state = { ...defaultState, ...this.state }; // 合并默认状态
}
NewClass.prototype = Object.create(OriginClass.prototype); // 继承原型
NewClass.prototype.constructor = NewClass;
return NewClass; // 返回新类替换原类
};
}
// 使用装饰器(传入默认状态参数)
@injectDefaultState({ status: 'active', role: 'user' })
class User {
constructor(name) {
this.name = name;
// 实例可自定义 state,会覆盖默认值
this.state = { role: 'admin' };
}
}
// 验证效果:state 合并默认值,自定义属性覆盖默认值
const user = new User('李四');
console.log(user.state); // { status: 'active', role: 'admin' }
2. 方法装饰器(装饰类的原型方法/静态方法)
作用
- 日志打印(调用前/后记录参数、返回值);
- 权限校验(执行方法前判断权限);
- 缓存优化(缓存方法返回结果,避免重复计算);
- 异常捕获(统一捕获方法执行错误)。
语法
装饰器函数接收 3 个参数:
target:原型方法 → 类的原型;静态方法 → 类本身;name:目标方法的名称;descriptor:目标方法的属性描述符(核心操作value)。
实战场景 1:日志打印(最常用)
js
// 装饰器:记录方法调用日志(支持传参:是否打印返回值)
function methodLog(showResult = true) {
return function(target, name, descriptor) {
const originFn = descriptor.value;
descriptor.value = async function(...args) {
// 调用前日志
const startTime = Date.now();
console.log(`[${new Date().toISOString()}] 方法 ${name} 开始调用,参数:`, args);
let result;
try {
result = await originFn.apply(this, args); // 支持异步方法
// 调用成功日志
if (showResult) console.log(`[${new Date().toISOString()}] 方法 ${name} 调用成功,返回值:`, result);
console.log(`[${new Date().toISOString()}] 方法 ${name} 执行耗时:${Date.now() - startTime}ms`);
} catch (err) {
// 调用失败日志
console.error(`[${new Date().toISOString()}] 方法 ${name} 调用失败,错误:`, err);
throw err; // 抛出错误,不阻断业务逻辑
}
return result;
};
return descriptor;
};
}
class OrderService {
// 装饰原型方法(异步),传参:显示返回值
@methodLog(true)
async createOrder(price, goods) {
await new Promise(resolve => setTimeout(resolve, 100)); // 模拟接口请求
return { orderId: Date.now(), price, goods };
}
// 装饰静态方法,传参:不显示返回值
@methodLog(false)
static cancelOrder(orderId) {
if (!orderId) throw new Error('订单ID不能为空');
return true;
}
}
// 验证效果
new OrderService().createOrder(99, ['手机']); // 打印完整日志(含返回值+耗时)
OrderService.cancelOrder(''); // 打印错误日志,抛出异常
实战场景 2:缓存优化(避免重复计算)
js
// 装饰器:缓存方法返回值(key 为参数拼接,支持基本类型参数)
function cacheDecorator() {
return function(target, name, descriptor) {
const originFn = descriptor.value;
const cache = new Map(); // 缓存容器:key=参数字符串,value=返回值
descriptor.value = function(...args) {
const cacheKey = JSON.stringify(args); // 参数转字符串作为 key
// 命中缓存,直接返回
if (cache.has(cacheKey)) {
console.log(`方法 ${name} 命中缓存,参数:`, args);
return cache.get(cacheKey);
}
// 未命中缓存,执行原方法,存入缓存
const result = originFn.apply(this, args);
cache.set(cacheKey, result);
return result;
};
return descriptor;
};
}
class CalcService {
// 装饰计算方法(高耗时场景:如大数据排序、复杂公式计算)
@cacheDecorator()
sum(a, b) {
console.log(`执行 sum 计算:${a} + ${b}`);
return a + b;
}
}
const calc = new CalcService();
calc.sum(10, 20); // 未命中,执行计算,输出日志 → 30
calc.sum(10, 20); // 命中缓存,直接返回,输出缓存日志 → 30
calc.sum(30, 40); // 未命中,执行计算 → 70
3. 属性装饰器(装饰类的原型属性/静态属性)
作用
- 限制属性的取值/赋值规则(如类型校验、范围限制);
- 给属性设置默认值;
- 监听属性变化(类似 Vue 的 watch)。
语法
装饰器函数接收 2 个参数(无 descriptor,需手动获取/修改):
target:原型属性 → 类的原型;静态属性 → 类本身;name:目标属性的名称。
实战场景:属性类型校验(防止赋值错误)
js
// 装饰器:校验属性类型(接收允许的类型数组,如 [String, Number])
function validateType(allowTypes) {
return function(target, name) {
let value; // 存储属性实际值
// 重新定义属性,通过 get/set 实现类型校验
Object.defineProperty(target, name, {
get() {
return value; // 取值时返回存储的值
},
set(newVal) {
// 校验新值类型是否在允许范围内
const isLegal = allowTypes.some(type => newVal instanceof type);
if (isLegal) {
value = newVal;
} else {
const typeNames = allowTypes.map(t => t.name).join('/');
console.error(`属性 ${name} 类型错误,允许类型:${typeNames},当前类型:${newVal?.constructor?.name}`);
}
},
enumerable: true, // 允许遍历属性
configurable: true
});
};
}
class User {
// 原型属性:仅允许 String 类型
@validateType([String])
name;
// 静态属性:仅允许 Number 类型
@validateType([Number])
static ageLimit;
}
// 验证效果
const user = new User();
user.name = '张三'; // 合法,赋值成功
user.name = 123; // 非法,打印错误,不赋值
console.log(user.name); // 张三
User.ageLimit = 18; // 合法,赋值成功
User.ageLimit = '18'; // 非法,打印错误,不赋值
console.log(User.ageLimit); // 18
4. 访问器装饰器(装饰类的 get/set 方法)
作用
- 增强 get 方法(如返回值格式化);
- 增强 set 方法(如赋值前数据清洗、权限校验)。
语法
与方法装饰器一致,接收 target、name、descriptor,descriptor 含 get(取值函数)和 set(赋值函数)属性。
实战场景:属性赋值清洗(去除字符串空格)
js
// 装饰器:清洗字符串属性(去除首尾空格,空字符串转为 null)
function trimString(target, name, descriptor) {
const originSet = descriptor.set; // 保存原 set 方法
// 重写 set 方法,添加清洗逻辑
descriptor.set = function(newVal) {
let cleanVal = newVal;
if (typeof newVal === 'string') {
cleanVal = newVal.trim(); // 去除首尾空格
if (cleanVal === '') cleanVal = null; // 空字符串转 null
}
originSet.call(this, cleanVal); // 执行原 set 方法
};
return descriptor;
}
class Product {
constructor() {
this._title = ''; // 私有属性(约定俗成)
}
// 装饰访问器 set 方法
@trimString
set title(val) {
this._title = val;
}
get title() {
return this._title || '无标题';
}
}
// 验证效果
const product = new Product();
product.title = ' 手机 '; // 赋值带空格的字符串
console.log(product.title); // 手机(空格被去除)
product.title = ' '; // 赋值全空格字符串
console.log(product.title); // 无标题(空字符串转 null,get 方法返回默认值)
5. 参数装饰器(装饰方法的参数)
作用
- 标记参数(如标记"必填参数");
- 校验参数合法性(如参数非空、范围校验)。
语法
装饰器函数接收 3 个参数:
target:原型方法 → 类的原型;静态方法 → 类本身;name:目标方法的名称;index:当前参数在方法参数列表中的索引(从 0 开始)。
实战场景:标记必填参数(校验参数非空)
js
// 1. 存储必填参数的容器(key:方法名,value:必填参数索引数组)
const requiredParams = new Map();
// 2. 参数装饰器:标记参数为必填
function required(target, name, index) {
if (!requiredParams.has(name)) {
requiredParams.set(name, []);
}
requiredParams.get(name).push(index); // 记录必填参数的索引
}
// 3. 方法装饰器:校验必填参数(需配合参数装饰器使用)
function checkRequired(target, name, descriptor) {
const originFn = descriptor.value;
descriptor.value = function(...args) {
// 获取当前方法的必填参数索引
const requiredIndexes = requiredParams.get(name) || [];
// 校验每个必填参数是否为空
for (const index of requiredIndexes) {
const param = args[index];
if (param === undefined || param === null || param === '') {
throw new Error(`方法 ${name} 的第 ${index + 1} 个参数为必填项,不可为空`);
}
}
return originFn.apply(this, args);
};
return descriptor;
}
class LoginService {
// 方法装饰器(校验必填)+ 参数装饰器(标记必填)
@checkRequired
login(@required username, @required password, rememberMe = false) {
console.log(`登录:用户名=${username},记住密码=${rememberMe}`);
return true;
}
}
// 验证效果
const loginService = new LoginService();
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. 可组合使用
多个装饰器可叠加在同一目标上,执行顺序为「从上到下定义,从下到上执行」(类似洋葱模型):
js
// 定义 3 个简单装饰器
function decoratorA(target, name, descriptor) {
console.log('执行装饰器 A');
return descriptor;
}
function decoratorB(target, name, descriptor) {
console.log('执行装饰器 B');
return descriptor;
}
function decoratorC(target, name, descriptor) {
console.log('执行装饰器 C');
return descriptor;
}
class Test {
// 装饰器顺序:A → B → C(定义顺序),执行顺序:C → B → A
@decoratorA
@decoratorB
@decoratorC
fn() {}
}
new Test().fn(); // 输出:执行装饰器 C → 执行装饰器 B → 执行装饰器 A
五、实际项目中的应用方案(前端+Node.js)
1. 前端项目(Vue3/React)
场景 1:Vue3 组件装饰器(配合 vue-class-component)
Vue3 支持通过装饰器简化组件逻辑(需安装依赖 vue-class-component):
vue
<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):
jsx
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 中间件装饰器)
给接口方法添加日志,记录请求参数、响应结果、耗时:
js
// 装饰器:接口请求日志
function apiLog(target, name, descriptor) {
const originFn = descriptor.value;
descriptor.value = async function(req, res, next) {
const startTime = Date.now();
console.log(`[API请求] 路径:${req.path},方法:${req.method},参数:`, req.body);
try {
const result = await originFn.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);
}
};
return descriptor;
}
// 服务层:用户接口
class UserController {
// 装饰接口方法
@apiLog
async getUserList(req) {
const { page = 1, size = 10 } = req.query;
// 业务逻辑:查询数据库
return { list: [{ id: 1, name: '张三' }], total: 1 };
}
@apiLog
async createUser(req) {
const { name, age } = req.body;
if (!name) throw new Error('用户名必填');
// 业务逻辑:插入数据库
return { id: Date.now(), name, age };
}
}
// Express 路由注册
const express = require('express');
const app = express();
const userController = new UserController();
app.use(express.json());
app.get('/api/users', userController.getUserList);
app.post('/api/users', userController.createUser);
app.listen(3000);
场景 2:数据库操作缓存(Redis 装饰器)
给数据库查询方法添加 Redis 缓存,减少数据库压力:
js
const redis = require('redis');
const client = redis.createClient({ url: 'redis://localhost:6379' });
client.connect();
// 装饰器:Redis 缓存(key 前缀+参数拼接,过期时间 5 分钟)
function redisCache(prefix = 'cache:', expire = 300) {
return async function(target, name, descriptor) {
const originFn = descriptor.value;
descriptor.value = async function(...args) {
const cacheKey = `${prefix}${name}:${JSON.stringify(args)}`;
// 先查 Redis 缓存
const cacheData = await client.get(cacheKey);
if (cacheData) {
console.log(`Redis 缓存命中,key:${cacheKey}`);
return JSON.parse(cacheData);
}
// 缓存未命中,查数据库
const data = await originFn.apply(this, args);
// 存入 Redis,设置过期时间
await client.setEx(cacheKey, expire, JSON.stringify(data));
console.log(`Redis 缓存存入,key:${cacheKey},过期时间:${expire}s`);
return data;
};
return descriptor;
};
}
// 数据层:用户数据库操作
class UserDao {
// 装饰查询方法,添加 Redis 缓存
@redisCache('user:')
async selectUserById(id) {
console.log(`查询数据库:用户ID=${id}`);
// 模拟数据库查询
return { id, name: '张三', age: 20 };
}
}
// 验证效果
const userDao = new UserDao();
userDao.selectUserById(1); // 查数据库,存入缓存
userDao.selectUserById(1); // 查 Redis 缓存,不查数据库
六、浏览器兼容性方案
装饰器是 ES7 提案,原生浏览器不支持(仅部分版本 Chrome 开启实验性标志支持),需通过工具编译为 ES5/ES6 代码,主流方案如下:
1. 核心编译工具:Babel
步骤 1:安装依赖
bash
# 核心依赖:Babel 预设 + 装饰器插件
npm install @babel/core @babel/cli @babel/preset-env @babel/plugin-proposal-decorators @babel/plugin-proposal-class-properties --save-dev
步骤 2:配置 Babel(.babelrc 或 babel.config.json)
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:编译代码
bash
# package.json 添加脚本
"scripts": {
"build": "babel src --out-dir dist" // 将 src 目录代码编译到 dist 目录
}
# 执行编译
npm run build
2. 工程化项目集成(Vue3/Vite/React)
场景 1:Vue3 + Vite(无需额外配置 Babel)
Vite 内置 @vitejs/plugin-vue-jsx 插件,支持装饰器(需配置 legacy: true):
js
// vite.config.js
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import vueJsx from '@vitejs/plugin-vue-jsx';
export default defineConfig({
plugins: [
vue(),
vueJsx({
// 启用装饰器支持
plugins: [
['@babel/plugin-proposal-decorators', { version: 'legacy' }],
['@babel/plugin-proposal-class-properties', { loose: true }]
]
})
]
});
场景 2:React + Webpack(Create React App 配置)
js
// webpack.config.js
module.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 中开启配置:
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. 日志装饰器(支持同步/异步、自定义前缀)
js
/**
* 方法日志装饰器:记录调用参数、返回值、耗时、错误信息
* @param options 配置项 { prefix: 日志前缀, showResult: 是否显示返回值, showTime: 是否显示耗时 }
*/
export function log(options = {}) {
const { prefix = '[LOG]', showResult = true, showTime = true } = options;
return function (target, methodName, descriptor) {
const originFn = descriptor.value;
// 适配同步/异步方法
descriptor.value = async function (...args) {
const startTime = Date.now();
// 调用前日志
console.log(`${prefix} 方法 ${methodName} 开始调用,参数:`, args);
try {
const result = await originFn.apply(this, args);
// 调用成功日志
if (showTime) console.log(`${prefix} 方法 ${methodName} 执行耗时:${Date.now() - startTime}ms`);
if (showResult) console.log(`${prefix} 方法 ${methodName} 调用成功,返回值:`, result);
return result;
} catch (error) {
// 调用失败日志
console.error(`${prefix} 方法 ${methodName} 调用失败,错误:`, error);
throw error; // 抛出错误,不阻断业务
}
};
return descriptor;
};
}
2. 缓存装饰器(支持过期时间、手动清除)
js
/**
* 方法缓存装饰器:缓存返回值,避免重复计算/请求
* @param options 配置项 { expire: 过期时间(ms, 0=永久), cacheKeyFn: 自定义缓存key生成函数 }
*/
export function cache(options = {}) {
const { expire = 0, cacheKeyFn } = options;
const cacheMap = new Map(); // 缓存容器:key=缓存key,value={ data: 数据, expireTime: 过期时间 }
// 清除指定方法的缓存(静态方法,需手动调用)
cache.clear = function (methodName, ...args) {
const key = cacheKeyFn ? cacheKeyFn(args) : JSON.stringify(args);
const fullKey = `${methodName}_${key}`;
cacheMap.delete(fullKey);
console.log(`[CACHE] 清除方法 ${methodName} 的缓存,key:${fullKey}`);
};
return function (target, methodName, descriptor) {
const originFn = descriptor.value;
descriptor.value = async function (...args) {
// 生成缓存key(支持自定义生成逻辑)
const key = cacheKeyFn ? cacheKeyFn(args) : JSON.stringify(args);
const fullKey = `${methodName}_${key}`;
const cacheItem = cacheMap.get(fullKey);
// 缓存命中且未过期,直接返回
if (cacheItem) {
const { data, expireTime } = cacheItem;
if (expire === 0 || Date.now() < expireTime) {
console.log(`[CACHE] 方法 ${methodName} 命中缓存,key:${fullKey}`);
return data;
}
// 缓存过期,删除旧缓存
cacheMap.delete(fullKey);
}
// 缓存未命中,执行原方法
const data = await originFn.apply(this, args);
// 存入缓存(计算过期时间)
const expireTime = expire > 0 ? Date.now() + expire : Infinity;
cacheMap.set(fullKey, { data, expireTime });
console.log(`[CACHE] 方法 ${methodName} 缓存存入,key:${fullKey},过期时间:${expire || '永久'}ms`);
return data;
};
return descriptor;
};
}
3. 权限校验装饰器(支持角色/权限码校验)
js
/**
* 权限校验装饰器:执行方法前校验权限,无权限则抛出错误/执行回调
* @param options 配置项 { allowRoles: 允许的角色数组, allowPerms: 允许的权限码数组, noAuthCb: 无权限回调 }
*/
export function auth(options = {}) {
const { allowRoles = [], allowPerms = [], noAuthCb } = options;
if (allowRoles.length === 0 && allowPerms.length === 0) {
throw new Error('[AUTH] 权限装饰器需配置 allowRoles 或 allowPerms');
}
return function (target, methodName, descriptor) {
const originFn = descriptor.value;
descriptor.value = function (...args) {
// 假设从全局获取当前用户权限(实际项目需从Vuex/Redux/全局状态获取)
const currentUser = { role: 'user', perms: ['user:view'] }; // 示例数据
const hasRole = allowRoles.length === 0 || allowRoles.includes(currentUser.role);
const hasPerm = allowPerms.length === 0 || allowPerms.some(perm => currentUser.perms.includes(perm));
// 有权限:执行原方法;无权限:执行回调/抛错
if (hasRole && hasPerm) {
return originFn.apply(this, args);
} else {
if (typeof noAuthCb === 'function') {
noAuthCb(methodName, currentUser);
return;
}
throw new Error(`[AUTH] 无权限执行方法 ${methodName},当前角色:${currentUser.role},权限:${currentUser.perms.join(',')}`);
}
};
return descriptor;
};
}
4. 必填参数校验装饰器(支持多参数标记)
js
/**
* 标记参数为必填(配合 requiredCheck 方法装饰器使用)
*/
export function required(target, methodName, paramIndex) {
// 存储必填参数:key=方法名,value=必填参数索引数组
if (!target.__requiredParams__) {
target.__requiredParams__ = new Map();
}
if (!target.__requiredParams__.has(methodName)) {
target.__requiredParams__.set(methodName, []);
}
target.__requiredParams__.get(methodName).push(paramIndex);
}
/**
* 必填参数校验装饰器:校验标记为 @required 的参数是否为空
*/
export function requiredCheck(target, methodName, descriptor) {
const originFn = descriptor.value;
const requiredParams = target.__requiredParams__?.get(methodName) || [];
descriptor.value = function (...args) {
for (const index of requiredParams) {
const param = args[index];
// 判定为空:undefined/null/空字符串/空数组(可根据需求调整)
const isEmpty = param === undefined || param === null || param === '' || (Array.isArray(param) && param.length === 0);
if (isEmpty) {
throw new Error(`[PARAM] 方法 ${methodName} 的第 ${index + 1} 个参数为必填项,不可为空`);
}
}
return originFn.apply(this, args);
};
return descriptor;
}
工具库使用示例(Vue3 组件实战)
vue
<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 模块,简化语法:
ts
// 安装依赖:npm install pinia-class-decorator
import { defineStore } from 'pinia';
import { Store, State, Action, Getter } from 'pinia-class-decorator';
import { log, cache } from '@/utils/decorators';
// 装饰器定义 Pinia 模块
@Store
export default class UserStore extends Store {
// 状态(等价于 state: () => ({ ... }))
@State()
userInfo = { id: '', name: '', role: 'user' };
@State()
token = '';
// 计算属性(等价于 getters: { ... })
@Getter()
get isAdmin() {
return this.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分钟
async fetchUserInfo() {
// 模拟接口请求
const res = await fetch('/api/user/info').then(res => res.json());
this.userInfo = res.data;
return res.data;
}
}
2. Node.js 接口分层(装饰器统一处理跨域/限流)
在 Node.js 接口层用装饰器统一处理通用逻辑,简化中间件配置:
ts
// 1. 限流装饰器:限制接口请求频率(如10次/分钟)
export function rateLimit(options = { max: 10, windowMs: 60 * 1000 }) {
const { max, windowMs } = options;
const requestMap = new Map(); // key=IP,value={ count: 请求次数, lastTime: 最后请求时间 }
return function (target, methodName, descriptor) {
const originFn = descriptor.value;
descriptor.value = async function (req, res, next) {
const clientIp = req.ip; // 获取客户端IP
const now = Date.now();
const requestInfo = requestMap.get(clientIp) || { count: 0, lastTime: now };
// 超出时间窗口,重置请求次数
if (now - requestInfo.lastTime > windowMs) {
requestInfo.count = 1;
requestInfo.lastTime = now;
} else {
requestInfo.count++;
// 超出请求限制,返回429
if (requestInfo.count > max) {
return res.status(429).json({ code: 429, msg: '请求过于频繁,请稍后再试' });
}
}
requestMap.set(clientIp, requestInfo);
return originFn.apply(this, [req, res, next]);
};
return descriptor;
};
}
// 2. 接口使用装饰器(跨域+限流+日志)
class OrderController {
@cors() // 自定义跨域装饰器(简化cors中间件)
@rateLimit({ max: 5, windowMs: 30 * 1000 }) // 5次/30秒
@log({ prefix: '[ORDER-API]' })
async getOrderList(req, res) {
const orders = await OrderModel.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深比较参数;
js
// 自定义缓存key(处理引用类型)
@cache({
cacheKeyFn: (args) => {
const [user, page] = args;
return `${user.id}_${page}`; // 用对象的唯一标识生成key
}
})
async getUserOrders(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)。
新标准装饰器示例(未来语法)
js
// 类装饰器直接传参(新标准)
@injectDefaultState({ status: 'active' })
class User {}
// 方法装饰器返回新方法(新标准)
function log(target, methodName, { get, set }) {
return {
get() {
const fn = get.call(this);
return function(...args) {
console.log('调用方法:', methodName);
return fn.apply(this, args);
};
}
};
}
兼容建议 :目前生产环境仍优先使用 legacy 模式(Babel/TS 成熟支持),待新标准稳定后,可通过工具自动迁移语法。
最终总结
装饰器的核心是「无侵入式增强」,通过封装通用逻辑,实现代码的解耦与复用,是前端/Node.js 项目中提升开发效率的关键技巧。掌握「类/方法/属性」三大核心装饰器,结合通用工具库封装,可快速落地到 Vue3/React/Node.js 项目中,解决日志、缓存、权限等高频场景问题。
实际开发中,需注意装饰器执行顺序、异步处理、兼容性配置,避开常见坑点,同时关注 ES 标准进展,逐步适配新语法。建议团队内部统一装饰器规范,沉淀通用工具库,最大化发挥装饰器的价值。