TypeScript装饰器
在 TS 中,装饰器仍然是一项实验性特性,未来可能有所改变,所以如果你要使用装饰器,需要在 tsconfig.json
的编译配置中开启experimentalDecorators
,将它设为 true
。
装饰器定义
装饰器是一种新的声明,它能够作用于 类声明
、方法
、访问符
、属性
和 参数
上。使用 @
符号加一个名字来定义,如 @decorate
,这的 decorate
必须是一个函数或者求值后是一个函数,这个 decorate
命名不是写死的,是你自己定义的,这个函数在 运行的时候被调用
,被装饰的声明作为 参数
会自动传入。要注意装饰器要紧挨着要修饰的内容的前面,而且所有的装饰器不能用在声明文件(.d.ts)中,和任何外部上下文中。
先定义一个函数,然后这个函数有一个参数,就是要装饰的目标,装饰的作用不同,这个 target
代表的东西也不同。
装饰器是一个函数,给类,方法,属性,参数进行修饰,进行功能扩展。
装饰器工厂
可以传递参数。
装饰器工厂也是一个函数,它的返回值是一个函数,返回的函数作为装饰器的调用函数。如果使用装饰器工厂,那么在使用的时候,就要加上函数调用,如下:
1 | function setProp () { // 这是一个装饰器工厂 |
装饰器组合
多个装饰器可以同时应用到一个声明上,就像下面的示例:
- 书写在同一行上:
1 | @f @g x |
- 书写在多行上:
1 | @f |
多个装饰器的执行顺序:
- 装饰器工厂从上到下依次执行,但是只是用于返回函数但不调用函数;
- 装饰器函数从下到上依次执行,也就是执行工厂函数返回的函数。
1 | function setProp1 () { |
多个装饰器,会先执行装饰器工厂函数获取所有装饰器
,然后再从后往前执行装饰器的逻辑
。
装饰器求值
类的定义中不同声明上的装饰器将按以下规定的顺序引用:
- 参数装饰器,方法装饰器,访问符装饰器或属性装饰器应用到每个实例成员;
- 参数装饰器,方法装饰器,访问符装饰器或属性装饰器应用到每个静态成员;
- 参数装饰器应用到构造函数;
- 类装饰器应用到类。
类装饰器
类装饰器在类声明之前声明,类装饰器应用于类的声明。
类装饰器应用于类构造函数,可以用来监视,修改或替换类定义。
类装饰器表达式会在运行时当做函数被调用,类的构造函数作为其唯一的参数。
如果类装饰器返回一个值,它会使用提供的构造函数来替换类的声明。
通过装饰器,我们就可以修改类的原型对象和构造函数。
1 | function addName(constructor: any) { |
定义类 A 并没有定义属性 name,会报错,可以通过类型断言解决报错。
1 | const a: any = new A(); |
如果类装饰器返回一个值,那么会使用这个返回的值替换被装饰的类的声明,所以我们可以使用此特性修改类的实现。但是要注意的是,我们需要自己处理原有的原型链。我们可以通过装饰器,来覆盖类里一些操作,来看官方的这个例子:
1 | function classDecorator<T extends { new (...args: any[]): {} }>(target: T) { |
首先我们定义了一个装饰器,它返回一个类,这个类继承要修饰的类,所以最后创建的实例不仅包含原 Greeter 类中定义的实例属性,还包含装饰器中定义的实例属性。还有一个点,我们在装饰器里给实例添加的属性,设置的属性值会覆盖被修饰的类里定义的实例属性,所以我们创建实例的时候虽然传入了字符串,但是 hello 还是装饰器里设置的”override”。我们把这个例子改一下:
1 | function classDecorator(target: any): any { |
在这个例子中,我们装饰器的返回值还是返回一个类,但是这个类不继承被修饰的类了,所以最后打印出来的实例,只包含装饰器中返回的类定义的实例属性,被装饰的类的定义被替换了。
如果我们的类装饰器有返回值,但返回的不是一个构造函数(类),那就会报错了。
方法装饰器
方法装饰器用来处理类中方法,它可以处理方法的属性描述符,可以处理方法定义。方法装饰器在运行时也是被当做函数调用,含 3 个参数:
- 装饰静态成员时是类的构造函数,装饰实例成员时是类的原型对象;
- 成员的名字;
- 成员的属性描述符。
来看例子:
1 | function enumerable(bool: boolean) { |
这里的 @enumerable(false)
是一个 装饰器工厂
。 当装饰器 @enumerable(false)
被调用时,它会修改属性描述符的 enumerable
属性。
因为这个装饰器修饰在下面使用的时候修饰的是实例(或者实例继承的)的方法,所以装饰器的第一个参数是类的原型对象;第二个参数是这个方法名;第三个参数是这个属性的属性描述符的对象,可以直接通过设置这个对象上包含的属性描述符的值,来控制这个属性的行为。
如果 方法装饰器返回一个值
,那么会用这个值作为方法的属性描述符对象
:
1 | function enumerable(bool: boolean): any { |
我们在这个例子中,在方法装饰器中返回一个对象,对象中包含 value 用来修改方法,enumerable 用来设置可枚举性。我们可以看到最后打印出的 info.getAge()的结果为”not age”,说明我们成功使用 function () { return “not age” }
替换了被装饰的方法 getAge () { return this.age }
注意,当构建目标小于 ES5 的时候,方法装饰器的返回值会被忽略。
访问器装饰器
访问器装饰器声明在一个访问器的声明之前(紧靠着访问器声明)。访问器也就是 set 和 get 方法,一个在设置属性值的时候触发,一个在获取属性值的时候触发。
首先要注意一点的是,TS 不允许同时装饰一个成员的 get 和 set 访问器,只需要这个成员 get/set 访问器中定义在前面的一个即可。
访问器装饰器也有三个参数,和方法装饰器是一模一样的。来看例子:
1 | function enumerable(bool: boolean) { |
这里我们同时给 name 属性的 set 和 get 访问器使用了装饰器,所以在给定义在后面的 set 访问器使用装饰器时就会报错。经过 enumerable 访问器装饰器的处理后,name 属性变为了不可枚举属性。同样的,如果访问器装饰器有返回值,这个值会被作为属性的属性描述符。
属性装饰器
属性装饰器声明在属性声明之前,它有 2 个参数,和方法装饰器的前两个参数是一模一样的。属性装饰器没法操作属性的属性描述符,它只能用来判断某各类中是否声明了某个名字的属性。
1 | function printPropertyName(target: any, propertyName: string) { |
如果 属性装饰器返回一个值
,那么会用这个值作为属性的属性描述符对象
:
1 | function nameDecorator(target: any, key: string): any{ |
我们在这个例子中,在属性装饰器中返回一个对象,对象中包含 value 用来修改原型上的属性,writable 用来设置可写性。注意 writable 会影响实例上的属性赋值,如果设置为 false 会报错,赋值会报错。Cannot assign to read only property 'name' of object '#<Test>'
。同时注意,value是对应原型上的属性,不会影响实例的属性name。
参数装饰器
参数装饰器有 3 个参数,前两个和方法装饰器的前两个参数一模一样:
- 装饰静态成员时是类的构造函数,装饰实例成员时是类的原型对象;
- 成员的名字;
- 参数在函数参数列表中的索引。
参数装饰器的返回值会被忽略,来看下面的例子:
1 | function required(target: any, propertName: string, index: number) { |
这里我们在 getInfo 方法的第二个参数之前使用参数装饰器,从而可以在装饰器中获取到一些信息。
装饰器使用案例
1 | const userInfo: any = undefined; |
我们在这个例子中,可以通过 catchError
装饰器,灵活的捕获函数的异常。动态扩展了函数的行为,没有直接注入捕获错误的代码。代码更加优雅。