文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

探索TypeScript:装饰器

2024-11-30 01:19

关注

面向切面编程(AOP)  是一种编程范式,它允许我们分离横切关注点,藉此达到增加模块化程度的目标。它可以在不修改代码自身的前提下,给已有代码增加额外的行为(通知)

装饰器一般用于处理一些与类以及类属性本身无关的逻辑,例如: 一个类方法的执行耗时统计或者记录日志,可以单独拿出来写成装饰器。

看一下官方的解释更加清晰明了

装饰器是一种特殊类型的声明,它能够被附加到类声明,方法, 访问符,属性或参数上。 装饰器使用 @expression这种形式,expression求值后必须为一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入。

如果有使用过spring boot或者php的symfony框架的话,就基本知道装饰器的作用分别类似于以上两者注解和annotation,而node中装饰器用的比较好的框架是nest.js。不过不了解也没关系,接下来我就按我的理解讲解一下装饰器的使用。

不过目前装饰器还不属于标准,还在建议征集的第二阶段,但这并不妨碍我们在ts中的使用。只要在 tsconfig.json中开启 experimentalDecorators就可以使用了。

{  
    "compilerOptions": {  
        "target": "ES5",  
        "experimentalDecorators": true  
    }  
}

类装饰器

类装饰器仅接受一个参数,该参数表示类本身。

同时,如果类装饰器返回一个值,它会使用提供的构造函数来替换类的声明。

比如:

// 类装饰器,接受一个参数即为类本身
// 将装饰后的类以及类的原型全部冻结变为不可扩展以及不可修改
function freeze(constructor: Function) {
  Object.freeze(constructor); // 冻结装饰的类
  Object.freeze(constructor.prototype); // 冻结类的原型
}


// 调用 freeze 装饰装饰 BugReport
@freeze
class BugReport {
  static type = 'report'
}


BugReport.type = 'hello'
console.log(BugReport.type) // TypeError: Cannot assign to read only property 'type' of function 'class BugReport

同时类装饰器如果存在一个有效返回值,该返回值会替代被修饰类的构造函数返回的实例对象。比如:

function override(target: new () => any) {
  return class Child {

  }
}

@override // override 装饰器修改了 Parent class 返回的实例对象
class Parent {

}

const instance = new Parent()

console.log(instance) // Child {}

方法装饰器

方法装饰器是在方法声明之前声明的。方式装饰器可用于观察、修改或替换方法定义。

方法装饰器接受三个参数:

同时,如果方法装饰器返回一个值,它会被用作方法的属性描述符。

比如下面的例子,我们使用方法装饰器修改类的实例方法,将 greet 方法变为不可枚举:

function enumerable(value: boolean) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {

    console.log(target) // Greeter.prototype
    console.log(propertyKey) // greet

    // 将该方法(Greeter.prototype.greet) 变为不可枚举
    descriptor.enumerable = value;
  };
}


class Greeter {
  greeting: string;
  constructor(message: string) {
    this.greeting = message;
  }
 
  // @enumerable(false) 修饰实例方法,既修饰器第一个参数为 Greeter.prototype
  @enumerable(false)
  greet() {
    return "Hello, " + this.greeting;
  }
}

console.log(Object.keys(Greeter.prototype)) // []

属性访问器装饰器

属性访问器装饰器同样在属性访问器声明前使用,常用于观察、修改或替换属性访问器的定义。

当属性装饰器被调用时,和方法装饰器同样会接受三个参数,分别为:

同样,如果访问器装饰器返回一个值,它也会被用作方法的属性描述符。

比如,当我们使用装饰器来修饰当前类上的属性访问器时:

function baseLog(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  // 触发属性访问器时
  console.log(`Trigger getter(${target.name}/${propertyKey})`)
}

class Person {

  @baseLog
  static get username() {
    return '19Qingfeng'
  }
}

// Trigger getter(Person/username)
// 19Qingfeng
console.log(Person.username)

参数装饰器

同样,class 上每个方法的参数还存在参数修饰器。参数修饰器会为参数声明之前,同样具有三个参数:

我们依次来看看参数装饰器分别装饰类的构造函数、类的静态方法上的参数以及类的实例方法上的参数不同表现:

参数修饰器所在方法为修饰类的构造函数:

class Person {

  constructor(@logger name: string) {

  }
}


function logger(target: any, methodName: string | undefined, index: number) {
  console.log(target) // [Function: Person]
  console.log(methodName) // undefined
  console.log(index) // 0
}

至此所有常见的类装饰器都介绍完了,其实本质的装饰器函数入参都是一致的,第一个参数是装饰器所在的类名、第二个参数是装饰参数,接下来我们看一下装饰器的实现原理。

实现原理

我们将一个包含很多装饰器的类将ts代码编译成es5的打包结果如下:

// ....
// 属性装饰器
__decorate([propertyDecorators], Parent.prototype, 'company', undefined);
// 访问器属性装饰器(原型)
__decorate([accessorDecorator], Parent.prototype, 'gender', null);
// 方法装饰器 & 参数(实例方法)装饰器
__decorate(
  [methodDecorator, __param(0, paramDecorator)],
  Parent.prototype,
  'getName',
  null
);
// 访问器属性装饰器(实例)
__decorate([accessorDecorator], Parent, 'staticGender', null);
// 方法装饰器(实例)
__decorate([methodDecorator], Parent, 'getStaticName', null);
// 类装饰器 & 参数装饰器(类的构造函数)
Parent = __decorate([logger, __param(0, paramDecorator)], Parent);
return Parent;

会发现所有装饰器都在调用__decorate方法,并且不同的装饰器,对于__decorate方法的入参也是通用型很强。

然后我们再看一下具体的__decorate方法:

var __decorate = function (decorators, target, key, desc) {
 // 首先获得实参的个数
 var c = arguments.length,

 // 1. 如果实参个数小于 3 ,则表示该装饰器为 类装饰或者在构造函数上的参数装饰器
 // 2. 如果实参个数大于等于3, 则表示为非 1 情况的装饰器。
 // 2.1 此时根据传入的第四个参数,来判断是否存在属性描述
 // 如果 desc 传入 null,则获取当前 target key 的属性描述符给 r 赋值。比如访问器属性装饰器、方法装饰器
 // 相反如果传入非 null (通常为 undefined), 则直接返回 desc 。比如属性装饰器


 // 此时 r 根据不同情况,
 // 要么是传入的 target    (实参个数小于3)
 // 要么是 Object.getOwnPropertyDescriptor(target, key) (实参个数小于3,且 desc 为 null)
 // 要么是 undefined (实参个数小于3, desc 为 undefined)
   r =
     c < 3
       ? target
       : desc === null
       ? (desc = Object.getOwnPropertyDescriptor(target, key))
       : desc,
   d;
 for (var i = decorators.length - 1; i >= 0; i--) {
   // 从数组的末尾到首部依次遍历获得每一个装饰方法
   if ((d = decorators[i])) {
     // 同样判断参数个数
     // 1. 如果实参个数小于 3, 类装饰器/构造函数上的参数装饰
     // 此时 d 为当前装饰器方法, r 为传入的 target (Parent)
     // 此时直接使用当前装饰器进行调用,传入 d(r) 也就是 d(Parent)
     // 2. 如果实参个数大于 3 ,则调用当前装饰 d(target, key, r)
     // 3. 如果实参个数等于 3 , 则调用 d(target, key)
     // 同时为 r 重新赋值,交给下一次 for 循环遍历处理下一个装饰器函数
     r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
   }
 }
 // 最终装饰器函数会进行返回
 // 如果个数大于 3,并且 r 存在 则会返回 Object.defineProperty(target, key, r) ,将返回的 r 当作属性描述符定义在 target key 上
 // 最终返回 r 
 return c > 3 && r && Object.defineProperty(target, key, r), r;
};

函数最后都会返回r对象,一开始会给予实参个数以及特定参数进行判断处理,然后基于decorators、target获得所有装饰方法,然后拿到装饰类的原型。

最终,会返回处理后的装饰器方法 r,在类装饰器上我们会使用到返回后的 r 重新赋值给当前构造函数。

Parent = __decorate([logger, __param(0, paramDecorator)], Parent);

至此,深入浅出装饰器全过程结束。

来源:量子前端内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯