装饰器:给你的代码穿上品如的衣服

装饰器相信大家在开发中经常用到或者看到,就比如@xxx() ,装饰器是个好东西,用好了能让你的代码更简洁、更优雅,还能让你在同事面前装逼(当然是正经的装逼)。希望这篇文章能帮你更好地理解和使用装饰器。如果你有任何问题或者有更好的装饰器用法,欢迎交流!

装饰器的概念(这块借用 AI 老师对它的解释):

装饰器本质上是一个函数,它以另一个函数或类作为参数,并返回一个新的函数或类,这个新的函数或类包含了原函数或类的功能,同时还添加了额外的行为。这种设计模式类似于现实生活中的装饰,比如给一幅画加上画框,画框(装饰器)为画(被装饰的对象)增添了额外的美感与保护功能,但画本身的核心内容并未改变。

大家接触过NestJs的肯定对这玩意熟悉,基本上每个文件都有用到它,比如实体文件,dto文件,密密麻麻的全是装饰器,用它去做一些类型的约束检查,比如:

以上 @ApiProperty,@IsString,@IsByteLength...这些都是装饰器,但是这些都是使用别人封装好的,我们可以直接用就完了,也能从单词语义上读明白他们大概什么意思,但是会用不行啊,不符合当前社会风气,卷起来!!!。

那种@xxx()这种可以理解为装饰器工厂,可以接受一些参数,也就是一个高阶函数,然后普通装饰器就是一个函数,然后他们接受一些参数,那么好,现在就来看看装饰器应该怎么玩,装饰器分为:

  1. 类装饰(整容级改造)
ts 复制代码
// 给类添加超能力(尊嘟假嘟?)
function EnhanceClass(constructor: Function) {
  constructor.prototype.sayHello = () => "你好,我是练习时长两年半的前端练习生";
}

@EnhanceClass
class Developer {}

console.log(new Developer().sayHello()); // 输出练习生语录

constructor - 被装饰的类的构造函数。可以通过修改 constructor 来改变类的行为,例如添加新的属性或方法,或者修改原型链等

  1. 方法装饰器(给方法叠Buff)
ts 复制代码
// 防抖装饰器(防止手抖党的福音)
function Debounce(delay: number) {
  return function (target: any, methodName: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    let timer: NodeJS.Timeout;

    descriptor.value = function (...args: any[]) {
      clearTimeout(timer);
      timer = setTimeout(() => originalMethod.apply(this, args), delay);
    };
    return descriptor
  };
}

class SearchBox {
  @Debounce(300)  // 输入停顿时自动触发搜索
  search(keyword: string) {
    console.log(`Searching: ${keyword}`);
  }
}
  • target:对于静态方法,target 是类的构造函数;对于实例方法,target 是类的原型对象。
  • methodName:被装饰的方法的名称。
  • descriptor:该方法的属性描述符,包含 value(方法本身)、writableenumerableconfigurable 等属性。可以通过修改 descriptor 来改变方法的行为。
  1. 属性装饰器
ts 复制代码
// 属性监听器(比朝阳群众还敏锐)
function ObserveChanges(target: any, propertyName: string) {
  let value = target[propertyName];

  Object.defineProperty(target, propertyName, {
    get: () => value,
    set: (newValue) => {
      console.log(`${propertyName} changed from ${value} to ${newValue}`);
      value = newValue;
    }
  });
}

class UserProfile {
  @ObserveChanges  // 监控属性变化
  username = "coder";
}

const user = new UserProfile();
user.username = "techLead"; // 控制台输出变化
  • target:对于静态属性,target 是类的构造函数;对于实例属性,target 是类的原型对象。
  • propertyKey:被装饰的属性的名称。
  1. 参数装饰器
ts 复制代码
function parameterDecorator(target, propertyKey, parameterIndex) { console.log(`Decorating parameter at index ${parameterIndex} of method ${propertyKey} on`, target); } class MyClass { myMethod(@parameterDecorator arg1, arg2) { // 方法逻辑 } }
  • target:对于静态属性,target 是类的构造函数;对于实例属性,target 是类的原型对象。
  • propertyKey:包含该参数的方法的名称。
  • parameterIndex:参数在方法参数列表中的索引位置,从 0 开始计数。

实际场景

好了以上就是业务用的较多的装饰器示例还有他们对应参数的一些解释,示例可能看起来没有那么使用,比如上面的防抖,大家可能都是用lodash提供的或者自己封装一个工具函数,下面贴一些我自己项目用到的一些场景吧:

  1. 函数重试(withRetry) 有时候,调用外部接口可能会失败,这时候我们希望能自动重试几次。withRetry 装饰器就能帮我们实现这个功能。
ts 复制代码
// 网络请求自动重试(比甲方还执着)
function WithRetry({maxRetries = 3,intervalMs = 1000} = {}){
  return function (target: any, methodName: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    
    descriptor.value = async function (...args: any[]) {
      let retryCount = 0;
      
      while (retryCount <= maxRetries) {
        try {
          return await originalMethod.apply(this, args);
        } catch (error) {
          retryCount++;
          await new Promise(resolve => setTimeout(resolve, intervalMs));
          console.log(`第${retryCount}次尝试...`);
        }
      }
      throw new Error("我躺平了,重试不动了...");
    }
  }
}

class ApiService {
  @WithRetry(3) //最多重试3次
  async fetchData() {
    return axios.get("..."); // 假装这里有网络请求
  }
}
  1. 函数权限(role)有时候,我们希望某些方法只有特定角色的用户才能调用。role 装饰器就能帮我们实现这个功能。
ts 复制代码
function role(requiredRole: string) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        const originalMethod = descriptor.value;
        descriptor.value = function (...args: any[]) {
            if (this.userRole !== requiredRole) {
                throw new Error(`用户没有权限调用 ${propertyKey}`);
            }
            return originalMethod.apply(this, args);
        };
    };
}

class UserService {
    userRole: string;

    constructor(role: string) {
        this.userRole = role;
    }

    @role('admin')
    deleteUser(userId: string) {
        console.log(`删除用户 ${userId}`);
    }
}

然后用户权限给大家带出一个我项目中用到的一个知识点 位图 就是比如一个用户有多个角色,他既是已登录的,又是已经官方已认证用户,通常我们可能用两个字段去处理,isLogin , isCert,然后我们项目中是怎么做的,

ts 复制代码
export enum UserRole {
  IsLogin = 2 ** 0, // 1
  IsCert = 2 ** 1,  // 2
  Isxxx1 = 2 ** 2,  // 4
  Isxxx2 = 2 ** 3,  // 8
}

// 比如我们用户的role值是 6 
// 我们需要验证他是否有IsCert权限 
6 & UserRole.IsCert ?'有权限':'没权限'

// 在表单提交的时候他如果是两个角色那么可以:
UserRole.IsLogin | UserRole.Isxxx1 // 多个用 | 拼接就行

这种二进制位运算和 enum 来管理用户角色和权限,在存储效率、验证速度、灵活性、可维护性和语义清晰度等方面都具有显著的优势。好了跑题了,回到装饰器,装饰器还有很多很多使用的一些场景,就不一一列出来了,但是也有小伙伴说这些不是都可以用工具函数去代替吗,也行,个人喜爱吧!

然后使用前在 tsconfig.json 中进行一些配置

json 复制代码
{
    "compilerOptions": {
        "experimentalDecorators": true,
        "emitDecoratorMetadata": true
    }
}

总结

以上就是我自己对装饰器的一些理解吧,个人觉得还是挺好用的,我能想到的它的一些好处吧:

  1. 解耦业务逻辑:装饰器可以把一些重复的逻辑抽离出来,让你的代码看起来更干净。
  2. 增强功能:通过装饰器,你可以给类、方法、属性等加上额外的功能,而不需要修改它们的内部实现。
  3. 提高可读性 :装饰器能让代码的意图更加明确,比如你一看 @withRetry 就知道这个函数是带重试功能的。
相关推荐
烛阴43 分钟前
秒懂 JSON:JavaScript JSON 方法详解,让你轻松驾驭数据交互!
前端·javascript
拉不动的猪1 小时前
刷刷题31(vue实际项目问题)
前端·javascript·面试
zeijiershuai1 小时前
Ajax-入门、axios请求方式、async、await、Vue生命周期
前端·javascript·ajax
恋猫de小郭1 小时前
Flutter 小技巧之通过 MediaQuery 优化 App 性能
android·前端·flutter
只会写Bug的程序员1 小时前
面试之《webpack从输入到输出经历了什么》
前端·面试·webpack
拉不动的猪1 小时前
刷刷题30(vue3常规面试题)
前端·javascript·面试
狂炫一碗大米饭1 小时前
面试小题:写一个函数实现将输入的数组按指定类型过滤
前端·javascript·面试
最胖的小仙女1 小时前
通过动态获取后端数据判断输入的值打小
开发语言·前端·javascript
yzhSWJ2 小时前
Vue 3 中,将静态资源(如图片)转换为 URL
前端·javascript·vue.js
Moment2 小时前
🏞 JavaScript 提取 PDF、Word 文档图片,非常简单,别再头大了!💯💯💯
前端·javascript·react.js