装饰器是一种特殊类型的声明,它能够被附加到类声明,方法,访问符,属性或参数上。

装饰器使用 @expression 这种形式,expression求值后必须为一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入。

通俗的理解可以认为就是在原有代码外层包装了一层处理逻辑。

个人认为装饰器是一种解决方案,而并非是狭义的@Decorator,后者仅仅是一个语法糖罢了。

装饰器的类型有:类装饰器访问器装饰器属性装饰器方法装饰器参数装饰器,但是没有函数装饰器(function)。

装饰器执行时机

修饰器对类的行为的改变,是代码编译时发生的(不是TypeScript编译,而是js在执行机中编译阶段),而不是在运行时。这意味着,修饰器能在编译阶段运行代码。也就是说,修饰器本质就是编译时执行的函数。

启用装饰器

若要启用实验性的装饰器特性,你必须在命令行或 tsconfig.json 里启用 experimentalDecorators 编译器选项:

命令行:

  1. tsc --target ES5 --experimentalDecorators

tsconfig.json:

  1. {
  2. "compilerOptions": {
  3. "target": "ES5",
  4. "experimentalDecorators": true
  5. }
  6. }

定义装饰器

装饰器本身其实就是一个函数,理论上忽略参数的话,任何函数都可以当做装饰器使用。

  1. function helloWord(target: any) {
  2. console.log('hello World!');
  3. }
  4. @helloWord
  5. class HelloWordClass {
  6. }
  7. new HelloWordClass() // 输出 `hello World!`

其中 target 就是目标类的构造函数。

编译结果(ES5):

  1. "use strict";
  2. var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
  3. var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
  4. if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
  5. else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
  6. return c > 3 && r && Object.defineProperty(target, key, r), r;
  7. };
  8. function helloWord(target) {
  9. console.log('hello World!');
  10. }
  11. var HelloWordClass = /** @class */ (function () {
  12. function HelloWordClass() {
  13. }
  14. HelloWordClass = __decorate([
  15. helloWord
  16. ], HelloWordClass);
  17. return HelloWordClass;
  18. }());
  19. new HelloWordClass(); // 输出 `hello World!`

装饰器组合

多个装饰器可以同时应用到一个声明上,就像下面的示例:

书写在同一行上:

  1. @f @g x

书写在多行上:

  1. @f
  2. @g
  3. x

带参数的装饰器

  1. function Path(p1: string, p2: string) {
  2. return function (target: any) { // 这才是真正装饰器
  3. console.log(`${p1} ${p2}`);
  4. }
  5. }
  6. @Path("hello", "world")
  7. class HelloService {}
  8. new HelloService()

类装饰器

应用于类构造函数,其参数是类的构造函数。

注意class并不是像Java那种强类型语言中的类,而是JavaScript构造函数的语法糖。

  1. function addAge(args: number) {
  2. return function(target: Function) { // target 为 Function(构造函数)
  3. target.prototype.age = args;
  4. };
  5. }
  6. @addAge(18)
  7. class Hello {
  8. name: string;
  9. age!: number;
  10. constructor() {
  11. this.name = 'xiaoyu'
  12. }
  13. }
  14. console.log(Hello.prototype.age); // 18
  15. let hello = new Hello();
  16. console.log(hello.name); // xiaoyu
  17. console.log(hello.age); // 18

方法装饰器

它会被应用到方法的 属性描述符上,可以用来监视,修改或者替换方法定义。

方法装饰会在运行时传入下列3个参数:

  1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  2. 成员的名字。
  3. 成员的属性描述符{value: any, writable: boolean, enumerable: boolean, configurable: boolean}。
  1. function method(type: string) {
  2. return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  3. console.log(type)
  4. console.log('prop ' + propertyKey)
  5. console.log('desc ' + JSON.stringify(descriptor) + '\n\n')
  6. }
  7. }
  8. class Hello {
  9. name: string;
  10. age: number;
  11. constructor() {
  12. this.name = 'xiaoyu'
  13. this.age = 18
  14. }
  15. @method('instance')
  16. hello() {
  17. return 'instance method'
  18. // instance
  19. // prop hello
  20. // desc {"writable":true,"enumerable":false,"configurable":true}
  21. }
  22. @method('static')
  23. static run() {
  24. return 'static method'
  25. // static
  26. // prop run
  27. // desc {"writable":true,"enumerable":false,"configurable":true}
  28. }
  29. }

访问器装饰器

访问器装饰器应用于访问器的属性描述符,可用于观察,修改或替换访问者的定义。 访问器装饰器不能在声明文件中使用,也不能在任何其他环境上下文中使用(例如在声明类中)。

TypeScript不允许为单个成员装饰get和set访问器。相反,该成员的所有装饰器必须应用于按文档顺序指定的第一个访问器。这是因为装饰器适用于属性描述符,它结合了get和set访问器,而不是单独的每个声明。

访问器装饰器表达式会在运行时当作函数被调用,传入下列3个参数:

  • 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  • 成员的名字。
  • 成员的属性描述符。

如果代码输出目标版本小于ES5,Property Descriptor将会是undefined。

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

如果代码输出目标版本小于ES5返回值会被忽略。

下面是使用了访问器装饰器(@configurable)的例子,应用于Point类的成员上:

  1. function configurable(value: boolean) {
  2. return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  3. descriptor.configurable = value
  4. }
  5. }
  6. class Point {
  7. private _x: number;
  8. private _y: number;
  9. constructor(x: number, y: number) {
  10. this._x = x
  11. this._y = y
  12. }
  13. @configurable(false)
  14. get x() { return this._x }
  15. set x(value: number) {
  16. this._x = value
  17. }
  18. @configurable(false)
  19. get y() { return this._y }
  20. set y(value: number) {
  21. this._y = value
  22. }
  23. }

方法参数装饰器

参数装饰器表达式会在运行时当作函数被调用,传入下列3个参数:

  1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  2. 参数的名字。
  3. 参数在函数参数列表中的索引。
  1. let parseType: string = ''
  2. class Modal {
  3. @parseFunc
  4. public addOne(@parse('number') num: any) {
  5. console.log('num:', num)
  6. return num + 1
  7. }
  8. }
  9. // 在函数调用前执行格式化操作
  10. function parseFunc(target: any, name: any, descriptor: any) {
  11. const originalMethod = descriptor.value
  12. descriptor.value = function(arg: any) {
  13. switch (parseType) {
  14. case 'number':
  15. arg = Number(arg)
  16. break
  17. case 'string':
  18. arg = String(arg)
  19. break
  20. case 'boolean':
  21. arg = String(arg) === 'true'
  22. break
  23. }
  24. return originalMethod.call(this, arg)
  25. }
  26. return descriptor
  27. }
  28. // 向全局对象中添加对应的格式化信息
  29. // type可选为 number, string, boolean
  30. function parse(type: string) {
  31. return function(target: Function, name: string, index: number) {
  32. parseType = type
  33. }
  34. }
  35. let modalStr = new Modal()
  36. console.log(modalStr.addOne('10')) // 11
  37. let modalNum = new Modal()
  38. console.log(modalNum.addOne(20)) // 21
  39. let modalBool = new Modal()
  40. console.log(modalBool.addOne(true)) // 2

属性装饰器

属性装饰器表达式会在运行时当作函数被调用,传入下列2个参数:

  1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  2. 成员的名字。
  1. function log(target: any, propertyKey: string) {
  2. let value = target[propertyKey]
  3. // 用来替换的getter
  4. const getter = function() {
  5. console.log(`Getter for ${propertyKey} returned ${value}`)
  6. return value
  7. }
  8. // 用来替换的setter
  9. const setter = function(newVal: any) {
  10. console.log(`Set ${propertyKey} to ${newVal}`)
  11. value = newVal
  12. }
  13. // 替换属性,先删除原先的属性,再重新定义属性
  14. if (delete target[propertyKey]) {
  15. Object.defineProperty(target, propertyKey, {
  16. get: getter,
  17. set: setter,
  18. enumerable: true,
  19. configurable: true
  20. })
  21. }
  22. }
  23. class Calculator {
  24. @log
  25. public num!: number;
  26. square() {
  27. return this.num * this.num
  28. }
  29. }
  30. let cal = new Calculator()
  31. cal.num = 2
  32. console.log(cal.square())
  33. // Set num to 2
  34. // Getter for num returned 2
  35. // Getter for num returned 2
  36. // 4

装饰器加载顺序

  1. function ClassDecorator() {
  2. return function(target: any) {
  3. console.log('I am class decorator')
  4. }
  5. }
  6. function MethodDecorator() {
  7. return function(target: any, methodName: string, descriptor: PropertyDescriptor) {
  8. console.log('I am method decorator')
  9. }
  10. }
  11. function Param1Decorator() {
  12. return function(target: any, methodName: string, paramIndex: number) {
  13. console.log('I am parameter1 decorator')
  14. }
  15. }
  16. function Param2Decorator() {
  17. return function(target: any, methodName: string, paramIndex: number) {
  18. console.log('I am parameter2 decorator')
  19. }
  20. }
  21. function PropertyDecorator() {
  22. return function(target: any, propertyName: string) {
  23. console.log('I am property decorator')
  24. }
  25. }
  26. @ClassDecorator()
  27. class Hello {
  28. @PropertyDecorator()
  29. greeting!: string;
  30. @MethodDecorator()
  31. greet(@Param1Decorator() p1: string, @Param2Decorator() p2: string) { }
  32. }

输出结果:

  1. I am property decorator
  2. I am parameter2 decorator
  3. I am parameter1 decorator
  4. I am method decorator
  5. I am class decorator

从上述例子得出如下结论:

  1. 有多个参数装饰器时:从最后一个参数依次向前执行
  2. 方法和方法参数中参数装饰器先执行。
  3. 类装饰器总是最后执行。
  4. 方法和属性装饰器,谁在前面谁先执行。因为参数属于方法一部分,所以参数会一直紧紧挨着方法执行。

上述例子中属性和方法调换位置,输出如下结果:

  1. I am parameter2 decorator
  2. I am parameter1 decorator
  3. I am method decorator
  4. I am property decorator
  5. I am class decorator

参考资料