您好, 欢迎来到 !    登录 | 注册 | | 设为首页 | 收藏本站

TypeScript 装饰器(Decorator)

装饰器是一种特殊类型的声明,它能够附加到类声明、、访问符、、类的参数上,以达到扩展类的行为。

自从 ES2015 引入 class,当我们需要在多个不同的类之间共享或者扩展一些或行为的时候,会变得错综复杂,极其不优雅,这也是装饰器被提出的很重要的原因。

常见的装饰器有:类装饰器、装饰器、装饰器、参数装饰器。

装饰器的写法:普通装饰器(无法传参)、 装饰器工厂(可传参)。

装饰器是一项实验性特性,在未来的版本中可能会发生改变。

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

命令行:

tsc --target ES5 --experimentalDecorators

tscon.json:

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

装饰器允许你在类和定义的时候去注释或者它。装饰器是作用于的表达式,它接收三个参数 targetnamedescriptor ,然后可选性的返回被装饰之后的 descriptor 对象。

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

装饰器工厂就是简单的,它返回表达式,以供装饰器在运行时。

通过装饰器工厂,可以额外传参,普通装饰器无法传参

function log(param: string) {
  return function (target: any, name: string, descriptor: PropertyDescriptor) {
    console.log('target:', target)
    console.log('name:', name)
    console.log('descriptor:', descriptor)

    console.log('param:', param)
  }
}

class Employee {

  @log('with param')
  routine() {
    console.log('Daily routine')
  }
}

const e = new Employee()
e.routine()

解释:

第 1 行,声明的 log() 就是装饰器,通过装饰器工厂这种写法,可以接收参数。

来看的打印结果:

target: Employee { routine: [Function] }
name: routine
descriptor: {
  value: [Function],
  writable: true,
  enumerable: true,
  conurable: true
}
param: with param
Daily routine

可以看到,先执行装饰器,然 routine() 。至于类装饰器表达式的三个参数 targetnamedescriptor 之后会单独介绍。

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

@f @g x
@f
@g
x

在 TypeScript 里,当多个装饰器应用在声明上时会进行如下步骤的操作:

通过下面的例子来观察它们求值的顺序:

function f() {
  console.log('f(): evaluated');
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log('f(): called');
  }
}

function g() {
  console.log('g(): evaluated');
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log('g(): called');
  }
}

class C {
  @f()
  @g()
  method() {}
}

在控制台里会打印出如下结果:

f(): evaluated
g(): evaluated
g(): called
f(): called

类装饰器表达式会在运行时当作被,类的构造作为其唯一的参数。

通过类装饰器扩展类的和:

function extension<T extends { new(...args:any[]): {} }>(constructor: T) {
  // 重载构造
  return class extends constructor {
    // 扩展
    public coreHour = '10:00-15:00'
    // 重载
    meeting() {
      console.log('重载:Daily meeting!')
    }
  }
}

@extension
class Employee {
  public name!: string
  public department!: string

  constructor(name: string, department: string) {
    this.name = name
    this.department = department
  }

  meeting() {
    console.log('Every Monday!')
  }

}

let e = new Employee('Tom', 'IT')
console.log(e) // Employee { name: 'Tom', department: 'IT', coreHour: '10:00-15:00' }
e.meeting()    // 重载:Daily meeting!

表达式的写法:

const extension = (constructor: Function) => {
  constructor.prototype.coreHour = '10:00-15:00'

  constructor.prototype.meeting = () => {
    console.log('重载:Daily meeting!');
  }
}

@extension
class Employee {
  public name!: string
  public department!: string

  constructor(name: string, department: string) {
    this.name = name
    this.department = department
  }

  meeting() {
    console.log('Every Monday!')
  }

}

let e: any = new Employee('Tom', 'IT')
console.log(e.coreHour) // 10:00-15:00
e.meeting()             // 重载:Daily meeting!

解释:

以上两种写法,其实本质是相同的,类装饰器表达式将构造作为唯一的参数,主要用于扩展类的和。

作用于类的装饰器表达式会在运行时当作被,传入下列3个参数 targetnamedescriptor

如果你熟悉 Object.defineProperty,你会立刻发现这正是 的三个参数。

比如通过修饰器完成只读,其实就是数据描述符中的 writable 的值 :

function readonly(value: boolean) {
  return function (target: any, name: string, descriptor: PropertyDescriptor) {
    descriptor.writable = value
  }
}

class Employee {
  @readonly(false)
  salary() {
    console.log('这是个秘密')
  }
}

const e = new Employee()
e.salary = () => { // Error,不可写
  console.log('change')
}
e.salary()

解释: 因为 readonly 装饰器将数据描述符中的 writable 改为不可写,所以倒数第三行报错。

参数装饰器表达式会在运行时当作被,以使用参数装饰器为类的原型上附加一些元数据,传入下列3个参数 targetnameindex

注意第三个参数的不同。

function log(param: string) {
  console.log(param)

  return function (target: any, name: string, index: number) {
    console.log(index)
  }
}

class Employee {

  salary(@log('IT') department: string, @log('John') name: string) {
    console.log('这是个秘密')
  }

}

可以用参数装饰器来监控的参数是否被传入。

function extension(params: string) {
  return function (target: any) {
    console.log('类装饰器')
  }
}

function method(params: string) {
  return function (target: any, name: string, descriptor: PropertyDescriptor) {
    console.log('装饰器')
  }
}

function attribute(params: string) {
  return function (target: any, name: string) {
    console.log('装饰器')
  }
}

function argument(params: string) {
  return function (target: any, name: string, index: number) {
    console.log('参数装饰器', index)
  }
}

@extension('类装饰器')
class Employee{
  @attribute('装饰器')
  public name!: string

  @method('装饰器')
  salary(@argument('参数装饰器') name: string, @argument('参数装饰器') department: string) {}
}

查看运行结果:

装饰器
参数装饰器 1
参数装饰器 0
装饰器
类装饰器

虽然装饰器还在草案阶段,但借助 TypeScript 与 Babel(需安装 babel-plugin-transform-decorators-legacy ) 这样的工具已经被应用于很多基础库中,当需要在多个不同的类之间共享或者扩展一些或行为时,可以使用装饰器简化。


联系我
置顶