TypeScript里的装饰器(Decorators)是什么?

546 阅读4分钟

上篇文章有提到装饰器,但是没有过多解释,这篇文章主要讨论装饰器,官方文档有详细的解释,希望我能给大家多提供一个视角来观察装饰器。这里不会面面俱到的把文档整理过来,这里尽量做到简洁实用,会用及在项目中使用之后,如果遇到复杂的问题不能解决,去官方文档或者有针对性的搜索,应该更好。

如何启用?

因为装饰器还是JavaScript里的实验性特性,所以必须在配置文件里启用它,一般在根目录下建tsconfig.json文件,必须包含以下内容:

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

如何使用?

装饰器主要分五类,下面一一介绍:

1. 类装饰器

const log = console.log

function setClassName (name: string) {
  return function <T extends { new (...args: any[]): {}}>(constructor: T) {
    return class extends constructor {
      __name = name
    }
  }
}

@setClassName('Teacher')
export default class Teacher { }

const teacher = new Teacher()
log({ Teacher, teacher })
log(teacher['__name'])

上面代码定义了一个setClassName装饰器,代码很简单,理解上应该没有难度。装饰器其实就普通的函数,只不过有一些规则。比如类装饰器接受唯一的一个参数,参数就是类本身。

该示例可以适用于,代码被压缩了,但是你想在运行时获取类名,装饰器用这种方式满足了你的需求。

2. 方法装饰器

function interceptor (before: Function, after: Function) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const value = descriptor.value as Function
    function fun (this: any, ...args: any[]) {
      before.apply(this, args)
      const res = value.apply(this, args)
      after.apply(this, [...args, res])
      return res
    }
    descriptor.value = fun
  }
}

export default class Teacher {
  salary: number = 10000
  bonus: number = 2000

  @interceptor((date:string) => log(`计算${date.getMonth() + 1}份薪资`), (date: string, res: number) => log('薪资支付完成:' + res))
  pay (date: string) {
    return this.salary + this.bonus
  }
}

const teacher = new Teacher()
teacher.pay('2020-11-11')

执行pay函数,会先后打印“计算2020-11-11薪资”和“薪资支付完成:12000”。

参数:

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

示例作用:

使用interceptor装饰器,可以在函数执行前做一些操作,比如,弹窗提示,比如埋点、比如参数拦截等等。也可以在函数执行后做一些处理,比如提示,结果格式化等等。

3. 访问器装饰器

function test (val: boolean) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    log(val, target, propertyKey, descriptor)
  }
}

@setClassName('Teacher')
export default class Teacher {
  _age: number = 18

  @test(false)
  get age () {
    return this._age
  }
  set age (val: number) {
    this._age = val
  }
}

访问器装饰器写法同方法装饰器一样,参数也一样,这里没想到合理的需求,写法上就这样了。

4. 属性装饰器

import 'reflect-metadata'

const log = console.log

function setClassName (name: string) {
  return function <T extends { new (...args: any[]): {}}>(constructor: T) {
    return class extends constructor {
+     constructor (...args: any[]) {
+       super(args)
+       const keys = Object.keys(this)
+       Reflect.getMetadataKeys(this).filter(key => keys.includes(key)).forEach(key => {
+         Object.defineProperty(this, key, Reflect.getMetadata(key, this))
+       })
+     }
      __name = name
    }
  }
}

function descriptor (val: PropertyDescriptor) {
  return function (target: any, propertyKey: string) {
    Reflect.defineMetadata(propertyKey, val, target)
  }
}

@setClassName('Teacher')
export default class Teacher {
  @descriptor({ enumerable: false })
  sex: string = '女'
}

const teacher = new Teacher()
log({ teacher, Teacher })
log(Object.keys(teacher))

为了上示例有一定的意义,这里结合了类装饰器。思路是这样的,先使用属性装饰器在对象上放一些metadata数据,类装饰器里的构造函数获取这些metadata数据,用这些数据做进一步处理。

参数

  1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  2. 成员的名字。

示例作用

用来设置属性的描述符。比如该示例log(Object.keys(teacher)),将打印出来[],一个空数组。

5. 参数装饰器

import 'reflect-metadata'

const log = console.log

function interceptor (before: Function, after: Function) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const fun = descriptor.value as Function
+   const matedata = Reflect.getMetadata(propertyKey, target)
    descriptor.value = function (...args: any[]) {
+     if (matedata) {
+       args[matedata.parameterIndex] = args[matedata.parameterIndex].substring(0, matedata.format.length)
+     }
      before.apply(this, args)
      const res = fun.apply(this, args)
      after.apply(this, [...args, res])
      return res
    }
  }
}

function formatDate (format: string) {
  return function (target: Object, propertyKey: string, parameterIndex: number) {
    Reflect.defineMetadata(propertyKey, { parameterIndex, format }, target)
  }
}

export default class Teacher {
  salary: number = 10000
  bonus: number = 2000

  @interceptor((date: string) => log(`计算${date}薪资`), (date: string, res: number) => log('薪资支付完成:' + res))
  pay (@formatDate('yyyy-MM') date: string) {
    return this.salary + this.bonus
  }
}

const teacher = new Teacher()
teacher.pay('2020-11-11')

参数装饰器结合了方法装饰器,可以看到,方法装饰器增加了4行代码,用来处理参数装饰器的数据。这里写的很简单,几乎没有实际作用,但是它具有参考价值,如果本篇文章写的太过复杂,反而掩盖主题思想。

参数

  1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  2. 成员的名字。
  3. 参数在函数参数列表中的索引。

示例作用

本来该示例是想做一个格式化参数的装饰器,这里简化了。当执行pay函数的时候,这次的输出,日期变成了'2020-11'

总结

以上就是typescript的五种装饰器,共同特点第一个参数是:对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。 也许有些地方看不太懂,这里还要强调一点儿:装饰是在运行时首先被执行的一段代码,也就是当node或者浏览器加载代码之后立即执行,先于所有的业务逻辑。上面的一些代码理解的时候,比如上面的方法装饰:拦截器,必须要明白装饰器是 运行时首先被执行的代码

本篇文章主要是介绍装饰器的使用方式,以及简单的介绍要了一些使用场景,后续或许会模仿Nest.js写一个简单的服务端框架,看了之后或许会明白Nest.js的实现原理。

上篇文章挖了一个坑要写装饰器,这篇文章又挖了一个坑简单实现Nest.js核心,希望自己能够把这个坑填上。

全部代码

展开

  import 'reflect-metadata'
  const log = console.log

function setClassName (name: string) {
return function <T extends { new (...args: any[]): {}}>(constructor: T) {
    return class extends constructor {
      constructor (...args: any[]) {
        super(args)
        const keys = Object.keys(this)
        Reflect.getMetadataKeys(this).filter(key => keys.includes(key)).forEach(key => {
          Object.defineProperty(this, key, Reflect.getMetadata(key, this))
        })
      }
      __name = name
    }
  }
}

function interceptor (before: Function, after: Function) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const fun = descriptor.value as Function
    const matedata = Reflect.getMetadata(propertyKey, target)
    descriptor.value = function (...args: any[]) {
      if (matedata) {
        args[matedata.parameterIndex] = args[matedata.parameterIndex].substring(0, matedata.format.length)
      }
      before.apply(this, args)
      const res = fun.apply(this, args)
      after.apply(this, [...args, res])
      return res
    }
  }
}

function test (val: boolean) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    log(val, target, propertyKey, descriptor)
  }
}

function descriptor (val: PropertyDescriptor) {
  return function (target: any, propertyKey: string) {
    Reflect.defineMetadata(propertyKey, val, target)
  }
}

function formatDate (format: string) {
  return function (target: Object, propertyKey: string, parameterIndex: number) {
    Reflect.defineMetadata(propertyKey, { parameterIndex, format }, target)
  }
}

@setClassName('Teacher')
export default class Teacher {
  salary: number = 10000
  bonus: number = 2000
  private _age: number = 18

  @descriptor({ enumerable: false })
  sex: string = '女'

  @interceptor((date: string) => log(`计算${date}薪资`), (date: string, res: number) => log('薪资支付完成:' + res))
  pay (@formatDate('yyyy-MM') date: string) {
    return this.salary + this.bonus
  }

  @test(false)
  get age () {
    return this._age
  }
  set age (val: number) {
    this._age = val
  }
}

const teacher = new Teacher()
log({ teacher, Teacher })
log(teacher['__name'])
teacher.pay('2020-11-11')
log(Object.keys(teacher))