TypeScript 装饰器(Decorator)
装饰器是一种特殊类型的声明,它能够附加到类声明、、访问符、、类的参数上,以达到扩展类的行为。
自从 ES2015 引入 class
,当我们需要在多个不同的类之间共享或者扩展一些或行为的时候,会变得错综复杂,极其不优雅,这也是装饰器被提出的很重要的原因。
常见的装饰器有:类装饰器、装饰器、装饰器、参数装饰器。
装饰器的写法:普通装饰器(无法传参)、 装饰器工厂(可传参)。
装饰器是一项实验性特性,在未来的版本中可能会发生改变。
若要启用实验性的装饰器特性,你必须在命令行或 tscon.json
里启用 experimentalDecorators
编译器选项:
命令行:
tsc --target ES5 --experimentalDecorators
tscon.json:
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true
}
}
装饰器允许你在类和定义的时候去注释或者它。装饰器是作用于的表达式,它接收三个参数 target
、 name
和 descriptor
,然后可选性的返回被装饰之后的 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()
。至于类装饰器表达式的三个参数 target
、name
、descriptor
之后会单独介绍。
多个装饰器可以同时应用到声明上,就像下面的示例:
@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个参数 target
、name
、descriptor
:
如果你熟悉 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个参数 target
、name
、index
:
注意第三个参数的不同。
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
) 这样的工具已经被应用于很多基础库中,当需要在多个不同的类之间共享或者扩展一些或行为时,可以使用装饰器简化。