JavaScript 装饰器完全指南(原理/分类/场景/实战/兼容)

JavaScript 装饰器(Decorator)是 ES7 提案中的特性,核心是通过"包装目标对象",在不修改原对象源码的前提下,动态扩展其功能,本质是"高阶函数的语法糖",让代码复用、功能增强更简洁优雅,已广泛应用于 React、Vue3、Node.js 等主流技术栈。

一、装饰器核心原理

1. 底层逻辑

装饰器本质是「接收目标对象、返回新对象(或修改原对象)的高阶函数」,核心流程:

  1. 拦截目标对象(类、方法、属性等)的定义/创建过程;
  2. 对目标对象进行功能增强(如添加日志、权限校验、缓存等);
  3. 返回增强后的对象,替换原目标对象生效。

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 个参数:

  1. target:原型方法 → 类的原型;静态方法 → 类本身;
  2. name:目标方法的名称;
  3. 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,需手动获取/修改):

  1. target:原型属性 → 类的原型;静态属性 → 类本身;
  2. 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、descriptordescriptorget(取值函数)和 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 个参数:

  1. target:原型方法 → 类的原型;静态方法 → 类本身;
  2. name:目标方法的名称;
  3. 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 个参数为空,抛出错误

三、装饰器的核心作用

  1. 解耦功能增强逻辑:将日志、权限、缓存等通用功能与业务逻辑分离,避免代码冗余(如每个方法都写一遍日志);
  2. 代码复用性极高:通用装饰器(如日志、缓存)可在全项目多个类/方法中复用,减少重复开发;
  3. 动态扩展功能:无需修改原代码,通过添加/删除装饰器,快速开启/关闭功能(如测试环境加日志,生产环境移除);
  4. 代码可读性提升@xxx 语法直观,一眼能看出目标对象的增强逻辑(如 @checkRequired 即知方法有必填校验);
  5. 符合开闭原则:对扩展开放(新增装饰器增强功能),对修改关闭(不改动原业务代码)。

四、装饰器的设计思路(落地核心)

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)可通过装饰器简化写法(如 withRouterconnect):

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. 兼容性注意事项

  1. 装饰器提案仍在迭代,legacy 模式是目前最稳定的兼容方案,避免使用 2023-05 等实验性版本;
  2. 低版本浏览器(如 IE11)需配合 core-js@3 引入 polyfill,处理 Map、Promise 等新特性;
  3. 避免在生产环境使用未编译的装饰器语法,会导致浏览器报错。

总结

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: trueemitDecoratorMetadata: true(参考前文兼容性配置)。

5. 装饰器修改原对象导致副作用

  • 问题:装饰器直接修改原方法/属性,导致其他地方使用原对象时逻辑异常;
  • 原理:装饰器应"包装"原对象,而非直接修改;
  • 解决方案 :先保存原对象(const originFn = descriptor.value),增强后通过 apply/call 执行,不直接覆盖原对象。

四、装饰器未来趋势(ES 标准进展)

目前装饰器处于 ES2023 提案阶段(Stage 3) ,与早期 legacy 模式相比,有 3 个核心变化:

  1. 装饰器返回值 :新标准装饰器返回「新对象」替换原对象,而非修改 descriptor
  2. 类装饰器支持参数:无需外层函数包裹,可直接给类装饰器传参;
  3. 私有属性装饰 :支持装饰类的私有属性(#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 标准进展,逐步适配新语法。建议团队内部统一装饰器规范,沉淀通用工具库,最大化发挥装饰器的价值。

相关推荐
瘦的可以下饭了3 小时前
3 链表 二叉树
前端·javascript
CreasyChan3 小时前
C# 委托/事件/UnityEvent 详解
开发语言·c#
whm27773 小时前
Visual Basic 建立数据库
开发语言·数据库·visual studio
1024小神4 小时前
swift中使用ObservableObject单利模式的时候,用let 或 @ObservedObject 或 @StateObject 有什么区别
开发语言·ios·swift
粉末的沉淀4 小时前
jeecgboot:electron桌面应用打包
前端·javascript·electron
烟西4 小时前
手撕React18源码系列 - Event-Loop模型
前端·javascript·react.js
空镜4 小时前
通用组件使用文档
前端·javascript
deng-c-f4 小时前
C/C++内置库函数(5):值/引用传递、移动构造、以及常用的构造技巧
开发语言·c++
豆约翰4 小时前
Z字形扫描ccf
java·开发语言·算法