装饰器

142 阅读10分钟

一、TS中类的装饰器

类的装饰器特性:

  1. 类的装饰器:对类进行修饰的工具
  2. 装饰器本身是一个函数
  3. 装饰器通过@符号来使用
  4. 装饰器的运行时刻(时机):创建(定义)类的时候,就会立即对类进行装饰,而不是对类进行实例化的时候
  5. 类装饰器接收的参数是构造函数
  6. 当有多个装饰器时,会从左到右,从上到下收集所有的装饰器。装饰器执行顺序:从下到上,从右到左。

环境搭建

npm init -y
tsc --init // 创建tsconfig.json文件
npm install ts-node -D 
npm install typescript --save
// package.json
"scripts": {
  "dev": "ts-node ./src/index.ts"
},
  1. 简单装饰器

    function testDecorator(constructor: any) { console.log('decorator') } @testDecorator class Test { } // 装饰器定义类的时候就会立即执行装饰器函数,对类进行装饰 const test = new Test() // new实例化的时候并不会执行装饰器函数

上述代码提示如下

提示装饰器是一项实验性的属性。 在tsconfig.json打开"experimentalDecorators": true,"emitDecoratorMetadata": true,对试验类型的支持属性就不会有此提示了。

  1. 多个装饰器的用法

    function testDecorator(constructor: any) { console.log('decorator') } function testDecorator1(constructor: any) { console.log('decorator') } @testDecorator @testDecorator1 class Test { }

如上所述:当有多个装饰器时,会从左到右,从上到下收集所有的装饰器。装饰器执行顺序:从下到上,从右到左

  1. 工厂模式的包装factory装饰器

3.1 语法提示不够完整

// 工厂模式 在一定条件下执行
function testDecorator(flag: boolean) {
  if (flag) {
    return function (constructor: any) {
      constructor.prototype.getName = () => {
        console.log('dell')
      }
    }
  } else {
    return function (constructor: any) { }
  }
}

@testDecorator(true) // 通过传入的参数控制装饰器
class Test { }
const test = new Test();
(test as any).getName()

3.2 增加的方法在原来的类上没有

// 工厂模式优化
function testDecorator<T extends new (...args: any[]) => {}>(constructor: T) {
  return class extends constructor {
    // 2.后执行
    name = 'bro' // 将传入的name覆盖
    getName() {
      return this.name
    }
  }
}

@testDecorator
class Test {
  name: string
  constructor(name: string) {
    // 1.先执行 扩展机制
    this.name = name
  }
}
const test = new Test('fruit');
console.log((test as any).getName())
// test.getName() 报错,是因为Test这个实例上本身没有getName方法,TS无法识别出来,是通过装饰器加上的

(...args: any[])含义:可以接收任意多个参数,将所有的参数合并到数组中。 new (...args: any[]) => {}代表一个构造函数,返回值为对象。 执行顺序,下执行Test实例中的constructor,再执行装饰器中的constructor

3.2 用装饰器修饰原来的类,使增加的方法在原来的类上可以直接访问

// 优化写法
function testDecorator() {
  return function <T extends new (...args: any[]) => {}>(constructor: T) {
    return class extends constructor {
      name = 'bro'
      getName() {
        return this.name
      }
    }
  }
}

const Test = testDecorator()(
  class {
    name: string
    constructor(name: string) {
      this.name = name
    }
  }
)

const test = new Test('fruit');
test.getName() // 此时不会报错,如下图

原因:现在的Test是装饰器装饰过后的类,已经装饰过后的类,Test上就有getName方法了,因此就可以识别了。

二、TS中的方法装饰器

普通方法,target对应的事类的prototype

方法构造器接收三个参数targetkeydescriptor

执行顺序:类的装饰器会在定义类的时候,立马执行。方法装饰器在类创建好之后,立即对方法做修改

静态方法statictarget对应的是类的构造函数

function getNameDecorator(target: any, key: string) {
  console.log(target, key)
}

class Test {
  name: string
  constructor(name: string) {
    this.name = name
  }
  @getNameDecorator
  static getName() {
    return 'this.name'
  }
}

参考文档: developer.mozilla.org/zh-CN/docs/…

接收三个参数Object.defineProperty(obj, prop, descriptor)

我们可以对getName方法进行修改

const test = new Test('fruit')
test.getName = () => { // 对getName方法做修改
  return '123'
}

如何让getName方法不做修改

function getNameDecorator(target: any, key: string, descriptor: PropertyDescriptor) {
  descriptor.writable = false // 设置为false,方法则不可被修改,如getName
  descriptor.value = function() { // value为属性或方法的原始值,
    return 'decorator'
  }
}


function getNameDecorator(target: any, key: string, descriptor: PropertyDescriptor) {
  // console.log(target, key)
  descriptor.writable = false // // 设置为false,方法则不可被修改,如getName
  descriptor.value = function () { // value为属性或方法的原始值,
    return 'decorator'
  }
}

class Test {
  name: string
  constructor(name: string) {
    this.name = name
  }
  @getNameDecorator
  getName() {
    return this.name
  }
}
const test = new Test('fruit')
console.log(test.getName()) // decorator

总结:装饰器对一个方法做完装饰之后,可以对target(原型)、key、descriptor都可以拿到,对方法可以做很多你想要的修改,这就是类class中方法装饰器的作用。

三、TS中访问器的装饰器

访问器:getter 、setter

访问器的装饰器的使用方法

function visitDecorator(target: any, key: string, descriptor: PropertyDescriptor) {}

class Test {
  private _name: string
  constructor(name: string) {
    this._name = name
  }
  get name() { // getter访问器
    return this._name
  }
  @visitDecorator // 给访问器增加装饰器
  set name(name: string) { // setter访问器
    this._name = name
  }
}
const test = new Test('fruit')
console.log(test.name) // fruit
test.name = '123'
console.log(test.name) // 123

增加descriptor.writable = false

function visitDecorator(target: any, key: string, descriptor: PropertyDescriptor) {
  descriptor.writable = false // 此访问器是不可修改的
}
...
test.name = '123' // 不能被重写

报错如下:

如果同时在setter和getter上用了相同的装饰器,就会报错

报错原因:TypeScript不允许同时装饰一个成员的get和set访问器。取而代之的是,一个成员的所有装饰的必须应用在文档顺序的第一个访问器上。这是因为,在装饰器应用于一个属性描述符时,它联合了get和set访问器,而不是分开声明的。简而言之:上述代码中name方法实际为一个属性,在setter写装饰器和在getter上写装饰器最终的效果都是一样的,两个上边都写,就会造成重复。

错误原因参考文档:www.tslang.cn/docs/handbo…

四、TS中的属性装饰器

  1. 属性装饰器也是Decorator的写法,接收两个参数target(原型)、key(属性的名字),可以通过返回descriptor来替换掉属性原始的descriptor
  2. 使用属性装饰器无法直接修改属性。修改的实际是原型的属性值。而类定义的属性是直接存储在类的实例上的。因此,修改原型上的内容并不会对实例上的属性有任何的变更影响。

下面是对以上两点的具体解释:

属性装饰器没有descriptor描述器

// 属性装饰器
function nameDecorator(target: any, key: string) {
  console.log(target, key)
}

class Test {
  @nameDecorator
  name = 'fruit'
}
const test = new Test()
console.log(test.name) // fruit

打印结果如下

创建descriptor,并返回,此时修改name属性就会报错

function nameDecorator(target: any, key: string): any {
  // 创建descriptor,并返回,会替换原始的descriptor
  const descriptor: PropertyDescriptor = {
    writable: false
  }
  return descriptor
}
class Test {
  @nameDecorator
  name = 'fruit' // 报错
}
const test = new Test()

错误如下:

把这段代码在TS官网上运行编译:

function nameDecorator(target: any, key: string): any {
  target[key] = 'bro'
}
class Test {
  @nameDecorator
  name = 'fruit' // 报错
}
const test = new Test()
console.log(test.name) // 输出为fruit,为什么不是bro呢?

输出为fruit,为什么不是bro呢?

如上图:Testname属性是在constructor构造器上的,而装饰器修改的是prototype上的name属性。修改的根本不是一个东西。而在运行时,先会找实例上的name属性,实例上有name属性,就不会再找prototype上的name属性了。实例上没有name属性的时候,才会找扩展的target的原型上的name属性。

// 修改的并不是实例上的name,而是原型上的name
function nameDecorator(target: any, key: string): any {
  target[key] = 'bro'
}
// name放在实例上
class Test {
  @nameDecorator
  name = 'fruit' // 报错
}
const test = new Test()
console.log((test as any).__proto__.name) // 通过__proto__属性就可以访问到prototype上的name属性

因此,想要使用属性装饰器,直接对属性的值做修改,实际上是做不到的。

五、TS中的参数装饰器

参数装饰器包含3个参数:

  1. target: 原型

  2. method: 方法名

  3. paramIndex: 参数所在的位置下标

    // target: 原型 method: 方法名 paramIndex: 参数所在的位置下标 function paramDecorator(target: any, method: string, paramIndex: number) { console.log(target, method, paramIndex) //Test { getInfo: [Function] } 'getInfo' 0 } class Test { getInfo(@paramDecorator name: string, age: number) { console.log(name, age) // fruit 18 } } const test = new Test() test.getInfo('fruit', 18)

六、TS中装饰器的使用例子

const userInfo: any = undefined
class Test {
  getName() {
    try {
      return userInfo.name
    } catch (error) {
      console.log('userInfo.name 不存在')
    }
  }
  getAge() {
    try {
      return userInfo.age
    } catch (error) {
      console.log('userInfo.age 不存在')
    }
  }
}
const test = new Test()
test.getName() // userInfo.name 不存在

如果用上述方法来处理异常,代码就会异常臃肿。我们可以用装饰器来解决。

方法一:普通方法,使用key来获取错误的方法名,但是错误提示不够明确,不能说明是userInfo.nameuserInfo.age有问题,只能说明此方法有问题

function catchError(target: any, key: string, descriptor: PropertyDescriptor) {
  const fn = descriptor.value // 对应的方法
  descriptor.value = function () {
    try {
      fn()
    } catch (error) {
      console.log(`userInfo ${key} error`)
    }
  }
}

const userInfo: any = undefined
class Test {
  @catchError
  getName() {
    return userInfo.name
  }
  @catchError
  getAge() {
    return userInfo.age
  }
}
const test = new Test()
test.getName() // userInfo getName error
test.getAge()  // userInfo getAge error

方法二: 使用工厂模式

function catchError(msg: string) {
  return function (target: any, key: string, descriptor: PropertyDescriptor) {
    const fn = descriptor.value
    descriptor.value = function () { // 重写方法
      try {
        fn() // 先执行原方法
      } catch (error) {
        console.log(msg)
      }
    }
  }
}


const userInfo: any = undefined
class Test {
  @catchError('userInfo.name error')
  getName() {
    return userInfo.name
  }
  @catchError('userInfo.age error')
  getAge() {
    return userInfo.age
  }
}
const test = new Test()
test.getName() // userInfo.name error
test.getAge() // userInfo.age error

七、reflect-metadata

reflect-metadata可以帮助我们在类或类的属性上存储一些元数据,并且方便的进行数据的反射和获取。

什么是元数据:就是类上面存储的一些额外的数据。元数据挂载对象上,但直接打印又看不到的形态,想做到这点就可以用reflect-metadata来帮助我么 参考链接:reflect-metadata

npm install reflect-metadata --save


import 'reflect-metadata'
const user = {
  name: 'fruit'
}
Reflect.defineMetadata('data', 'test', user)
console.log(user) // { name: 'fruit' }

最基础的定义元数据,获取元数据方法

import 'reflect-metadata'
const user = {
  name: 'fruit'
}
Reflect.defineMetadata('data', 'test', user) // 赋值
console.log(Reflect.getMetadata('data', user)) // 取值 test

在类上定义元数据

import 'reflect-metadata'
@Reflect.metadata('data', 'test') // 赋值
class User {
  name = 'dell'
}
console.log(Reflect.getMetadata('data', User)) // test

在类的属性或方法上定义元数据

// 在类的属性上定义元数据
import 'reflect-metadata'
class User {
  @Reflect.metadata('data', 'test') // 在类的属性上定义
  name = 'dell'
}
// 挂载在了类的原型链上
console.log(Reflect.getMetadata('data', User.prototype, 'name')) // test


// 在类的方法上定义元数据
import 'reflect-metadata'
class User {
  @Reflect.metadata('data', 'test')
  getName() { }
}
console.log(Reflect.getMetadata('data', User.prototype, 'getName')) // test

常用API:

hasMeatdata:判断当前的target(对象)上有没有对应的元数据

// 使用hasMeatdata
class User {
  @Reflect.metadata('data', 'test')
  getName() { }
}
console.log(Reflect.hasMetadata('data', User.prototype, 'getName')) // true

hasOwnMetadata:是否拥有改属性

mport 'reflect-metadata'
class User {
  @Reflect.metadata('data', 'test')
  getName() { }
}
class Teacher extends User { } // 继承
console.log(Reflect.hasMetadata('data', User.prototype, 'getName')) // true
console.log(Reflect.hasMetadata('data', Teacher.prototype, 'getName')) // true

console.log(Reflect.hasOwnMetadata('data', User.prototype, 'getName')) // true
console.log(Reflect.hasOwnMetadata('data', Teacher.prototype, 'getName')) // false

由于Teacher是的data是继承而来的,因此为false.

getMetadataKeys:获取类的方法上的元数据的名字有哪些

import 'reflect-metadata'
class User {
  @Reflect.metadata('data', 'test')
  getName() { }
}
console.log(Reflect.getMetadataKeys(User.prototype, 'getName'))

结果如下:

'design:returntype','design:paramtypes','design:type',为默认自身的类型

getOwnMetadataKeys: 和getOwnMetadata类似,

import 'reflect-metadata'
class User {
  @Reflect.metadata('data', 'test')
  getName() { }
}
class Teacher extends User { }
console.log(Reflect.getOwnMetadataKeys(User.prototype, 'getName')) 
console.log(Reflect.getOwnMetadataKeys(Teacher.prototype, 'getName'))

打印结果如下:TeachergetOwnMetadataKeys为空,继承的是没有自身的元数据的。

deleteMetadata:当我们在一个方法上定义了metadata元数据后,可以通过deleteMetadata进行删除

TS中利用reflect-metadata元数据的定义和反射获取元数据的机制,可以对我们的代码做哪些改良?

八、TS中装饰器的执行顺序

不同的装饰器结合在一起应该如何使用?他们的执行顺序是怎么样的?他们怎么和元数据做关联?

function showData(target: typeof User) {
  for (let key in target.prototype) {
    const data = Reflect.getMetadata('data', target.prototype, key)
    console.log(data)
  }
}

import 'reflect-metadata'
@showData
class User {
  @Reflect.metadata('data', 'name')
  getName() { }

  @Reflect.metadata('data', 'age')
  getAge() { }
}

打印结果如上图,说明方法的装饰器执行一定优先于类的装饰器,否则将无法获取到方法装饰器的值

封装类似装饰器setData,可以非常灵活的进行个性化定制:

function showData(target: typeof User) {
  for (let key in target.prototype) {
    const data = Reflect.getMetadata('data', target.prototype, key)
    console.log(data)
  }
}

function setData(dataKey: string, msg: string) {
  return function (target: User, key: string) {
    Reflect.defineMetadata(dataKey, msg, target, key)
  }
}

import 'reflect-metadata'
@showData
class User {
  @Reflect.metadata('data', 'name')
  getName() { }

  @setData('data', 'age')
  getAge() { }
}

打印结果如下,和第一个例子一致,说明使用Reflect.defineMetadata自定义的装饰器效果正常:

  1. 方法装饰器优先于类的装饰器执行,类的装饰器是最后执行的。
  2. Reflect.metadata的原理性,就是使用Reflect.defineMetadata来实现的。

推荐学习网站:

  1. www.typescriptlang.org/ TS官网,查看对应官方文档
  2. ts-ast-viewer.com/# 帮助我们将语法变成抽象语法书,进一步分析写的内容会转化为什么
  3. redux.js.org/recipes/usa… 在redux中如何使用TS,在redux官网搜索typescript即可.