找回密码
 立即注册
首页 业界区 业界 【每日一面】装饰器原理

【每日一面】装饰器原理

皮仪芳 2025-11-12 13:25:00
装饰器原理

基础问答

问:什么是装饰器?有什么作用?
答:装饰器是一种​元编程语法,可以在不修改原有代码的前提下,动态地为类、方法、属性等添加一些能力,本质上还是一个函数,它接收目标对象、属性名、属性描述符(或类本身)作为参数,返回修改后的目标对象或属性描述符。
在使用的时候,是声明式的使用,在装饰器函数前加上一个 @ 符号,在需要使用的函数、类、方法、属性上一行的位置添加,如下:
  1. // 定义类装饰器:为类添加版本信息
  2. function addVersion(version) {
  3.   // 装饰器函数接收类作为参数
  4.   return function (target) {
  5.     // 为类添加静态属性
  6.     target.version = version;
  7.     // 为类添加静态方法
  8.     target.logVersion = function () {
  9.       console.log(`版本号:${this.version}`);
  10.     };
  11.     // 返回修改后的类(也可返回新类)
  12.     return target;
  13.   };
  14. }
  15. // 使用装饰器修饰类
  16. @addVersion('1.0.0')
  17. class MyClass {
  18.   constructor(name) {
  19.     this.name = name;
  20.   }
  21. }
  22. // 测试效果
  23. console.log(MyClass.version); // 输出:1.0.0
  24. MyClass.logVersion(); // 输出:版本号:1.0.0
复制代码
如果你不想在本地运行,可以在  Typescript Playground 编译运行。
扩展延伸

先说个题外话,你需要知道的是,装饰器现在还是在提案阶段(TC39 Stage 3),自 2015 年提出以来,到现在依然没有成为规范的一部分,但是目前已经广泛的应用在前端的很多库中,如 MobX 、Angular 依赖注入等,只是我们在使用上略微复杂,需要通过一些编译工具(如 Babel)或 Typescript 来进行转换。
基本使用

装饰器可用于修饰​​、​类方法​、​类属性​、访问器(getter/setter)  等,不同修饰的场景下的语法和参数略有一些差异。

  • 类装饰器:一般用于修饰整个类,可以为类添加静态属性和方法(参考基础问答部分代码),也可以修改类的构造函数,示例如下:
    1. // 定义类装饰器:为实例添加默认属性
    2. function addDefaultProps(props: Record<string, any>) {
    3.   return function <T extends { new (...args: any[]): {} }>(target: T) {
    4.     // 返回一个新的类,继承自原类
    5.     return class extends target {
    6.       constructor(...args: any[]) {
    7.         super(...args);
    8.         // 添加默认属性
    9.         Object.assign(this, props);
    10.       }
    11.     };
    12.   };
    13. }
    14. // 使用装饰器
    15. @addDefaultProps({ type: 'base', status: 'active' })
    16. class MyClass {
    17.   name: string;
    18.   
    19.   constructor(name: string) {
    20.     this.name = name;
    21.   }
    22. }
    23. // 测试
    24. const instance = new MyClass('test');
    25. console.log(instance); // 输出:{ name: 'test', type: 'base', status: 'active' }
    复制代码
  • 方法装饰器:方法装饰器用于修饰类的方法,接收三个参数
    ​target:类的原型对象(静态方法则为类本身)
    ​propertyKey:方法名
    ​descriptor​:方法的属性描述符({ value, writable, enumerable, configurable })
    1. // 定义方法装饰器:记录方法调用日志
    2. function log(target, propertyKey, descriptor) {
    3.   // 保存原方法
    4.   const originalMethod = descriptor.value;
    5.   // 重写方法
    6.   descriptor.value = function (...args) {
    7.     console.log(`[日志] 调用方法 ${propertyKey},参数:`, args);
    8.     // 调用原方法并获取返回值
    9.     const result = originalMethod.apply(this, args);
    10.     console.log(`[日志] 方法 ${propertyKey} 返回:`, result);
    11.     return result;
    12.   };
    13.   // 返回修改后的描述符
    14.   return descriptor;
    15. }
    16. class Calculator {
    17.   // 使用装饰器修饰方法
    18.   @log
    19.   add(a, b) {
    20.     return a + b;
    21.   }
    22. }
    23. // 测试
    24. const calc = new Calculator();
    25. calc.add(2, 3);
    26. // 输出:
    27. // [日志] 调用方法 add,参数: [2, 3]
    28. // [日志] 方法 add 返回: 5
    复制代码
  • 属性装饰器:用于修饰类的属性,接收两个参数
    ​target:类的原型对象(静态属性则为类本身)
    ​propertyKey:属性名
    1. // 定义属性装饰器:限制属性值范围
    2. function range(min, max) {
    3.   return function (target, propertyKey) {
    4.     // 定义私有属性存储值(避免命名冲突)
    5.     const privateKey = `_${propertyKey}`;
    6.     // 通过Object.defineProperty定义属性
    7.     Object.defineProperty(target, propertyKey, {
    8.       get() {
    9.         return this[privateKey];
    10.       },
    11.       set(value) {
    12.         if (value < min || value > max) {
    13.           throw new Error(`${propertyKey} 必须在 ${min}-${max} 范围内`);
    14.         }
    15.         this[privateKey] = value;
    16.       }
    17.     });
    18.   };
    19. }
    20. class User {
    21.   @range(0, 120)
    22.   age;
    23.   constructor(age) {
    24.     this.age = age;
    25.   }
    26. }
    27. // 测试
    28. const user1 = new User(25);
    29. console.log(user1.age); // 输出:25
    30. const user2 = new User(150);
    31. // 抛出错误:age 必须在 0-120 范围内
    复制代码
  • 访问器装饰器:用于修饰类的 getter​ 或 setter​,参数与方法装饰器相同(target​、propertyKey​、descriptor),返回修改后的描述符
    1. // 定义访问器装饰器:过滤敏感字符
    2. function sanitize(target, propertyKey, descriptor) {
    3.   // 判断是getter还是setter
    4.   if (descriptor.get) {
    5.     const originalGet = descriptor.get;
    6.     descriptor.get = function () {
    7.       const value = originalGet.apply(this);
    8.       // 过滤HTML标签
    9.       return value.replace(/<[^>]+>/g, '');
    10.     };
    11.   }
    12.   return descriptor;
    13. }
    14. class Message {
    15.   constructor(content) {
    16.     this._content = content;
    17.   }
    18.   // 使用装饰器修饰getter
    19.   @sanitize
    20.   get content() {
    21.     return this._content;
    22.   }
    23. }
    24. // 测试
    25. const msg = new Message(' 正常内容');
    26. console.log(msg.content); // 输出:恶意代码 正常内容(已过滤<script>标签)
    复制代码
需要注意的是,JavaScript 没有支持装饰器,前面说了装饰器还在提案阶段,要使用这个特性,需要通过 Typescript 或 babel 等编译器,而且随着版本的更迭,有些写法会有不同,如果你运行了这个表格
工作原理

和 JavaScript 中的 class、async/await 类似,装饰器也是一个语法糖,底层还是通过函数调用实现。

  • 编译过程
    对于类装饰器:当使用装饰器函数修饰一个类的时候,顺序是,先定义这个类,然后在这个使用装饰器函数包裹这个类(作为参数传递),最后用函数返回值覆盖原来的类。
    你可以简单的视为:
    1. // 源码
    2. @decorator
    3. class MyClass {}
    4. // 编译后(近似)
    5. class MyClass {}
    6. MyClass = decorator(MyClass) || MyClass;
    复制代码
    更详细的编译结果,可以在自己运行一次 TypeScript 的编译得到。
    对于方法装饰器,很容易根据上面的想到,其编译过程是在类定义之后,将方法作为参数传递
  • 执行时机
    装饰器在​类定义阶段执行(而非实例化阶段),这意味着:

    • 装饰器的逻辑在类被定义时就会执行,而非调用方法或创建实例时;
    • 装饰器内部无法访问类的实例(this指向原型对象或类本身,而非实例)。
    1. function logWhenDefined(target) {
    2.   console.log('类被定义了!');
    3.   return target;
    4. }
    5. @logWhenDefined
    6. class MyClass {}
    7. // 输出:类被定义了!(此时还未创建实例)
    复制代码
面试追问


  • 装饰器和高阶函数的区别是什么?
    相同点:两者都可实现功能扩展,本质都是函数
    不同点

    • 装饰器是​语法糖​,有明确的语法规范(@符号),仅用于修饰类或类成员;
    • 高阶函数是​函数式编程概念,指接收函数作为参数或返回函数的函数,适用范围更广(可修饰任何函数,不限于类方法);
    • 装饰器在类定义阶段执行,高阶函数在函数调用阶段执行
    这里给出一个高阶函数示例:
    1. // 高阶函数实现日志功能(与方法装饰器效果类似)
    2. function withLog(fn) {
    3.   return function (...args) {
    4.     console.log('调用函数,参数:', args);
    5.     const result = fn.apply(this, args);
    6.     console.log('函数返回:', result);
    7.     return result;
    8.   };
    9. }
    10. // 用高阶函数修饰普通函数
    11. function add(a, b) {
    12.   return a + b;
    13. }
    14. const addWithLog = withLog(add);
    15. addWithLog(1, 2)
    复制代码
  • 装饰器,可以装饰函数和对象吗?
    装饰器仅支持类和类成员(方法、属性、访问器),不支持普通函数或对象,是因为函数存在函数(变量)提升,装饰器执行时机(定义阶段)与函数提升可能冲突,导致逻辑混乱,如果想实现类似的效果,建议是通过高阶函数来实现,参考上一问。
  • 实际开发过程中,你在什么场景下使用装饰器?

    • 日志与监控​:为方法添加调用日志、性能统计(如上述log​和measureTime装饰器);
    • 权限控制​:在需要权限的方法前添加权限校验(如requirePermission);
    • 缓存处理:为耗时方法添加结果缓存(避免重复计算);
    • 框架集成

      • Angular:用装饰器定义组件(@Component​)、服务(@Injectable);
      • MobX:用@observable​、@action装饰器管理状态;
      • Vue Class Component:用@Component​、@Prop装饰器定义 Vue 组件;

    • 数据校验:为类属性添加类型或范围校验(如@range装饰器)。

  • 使用装饰器的时候,遇到过什么问题?

    • 兼容性:装饰器仍为提案,需通过 Babel/TypeScript 转译,不同转译工具可能有语法差异;
    • 执行时机:装饰器在类定义时执行,避免在装饰器中编写依赖实例的逻辑;
    • 原型链影响:修改类或方法时需注意保持原型链完整(如单例装饰器中继承原类原型);
    • 性能开销:装饰器会增加函数调用层级,复杂装饰器可能影响性能(需适度使用)。

  • 你写的这个方法装饰器,为什么我运行报错?
    这个就是一个踩坑的地方,由于装饰器并没有正式的落地标准,所以你会发现有一些网上的装饰器代码你运行不起来,注意切换Typescript或babel的插件版本去解决。
    如本文中的代码,在 Typescript 3.x 版本中都可以正常使用,但是升级版本后有些就不兼容了。
参考文章

[1]: 【大前端】js装饰器的10年难产之路
友情链接:webfem.com
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册