Typescript装饰器

483 阅读8分钟

前言:

最近体验了一下midway这款框架,我的评价是真香。装饰器给代码层面上带来了简洁,让我们不再关注背后的实现,只需要知道有这么个方法,同时在读到官方文档依赖注入时,这是我不知凡几都熟悉但未曾深入理解的名词。

编程不仅仅是门技术,同时也是一门艺术

目录

  1. 装饰器
  2. Reflect Metadata
  3. AOP
  4. loc+DI

1.装饰器

介绍(引用中文手册)

随着TypeScript和ES6里引入了类,在一些场景下我们需要额外的特性来支持标注或修改类及其成员。 装饰器(Decorators)为我们在类的声明及成员上通过元编程语法添加标注提供了一种方式。 Javascript里的装饰器目前处在 建议征集的第二阶段,但在TypeScript里已做为一项实验性特性予以支持。

简单来说装饰器是一种特殊类型的声明,它能够被附加到类及其成员上,包括方法参数。装饰器可以说是一种解决方案,提供新的解决问题的方式。

优点

降低代码耦合度,实现非入侵式修改

缺点

需要学习成本

装饰器主要有以下几种:

  • 类装饰器
  • 属性装饰器
  • 方法装饰器
  • 访问器装饰器
  • 参数装饰器

1.类装饰器

在类声明之前被声明(紧靠着类声明)。 类装饰器应用于类构造函数,可以用来监视,修改或替换类定义。 类装饰器不能用在声明文件中( .d.ts),也不能用在任何外部上下文中(比如declare的类)。

/**
 * 类装饰器
 * 本质上是一个函数,且唯一参数是类的构造函数,可以返回一个新的构造函数

 *
 * 调用签名:
 * (target:object)=>void | typeof [target]
 * 这里[target]指当前类
 * 
 * 参数:
 *    target 类的构造函数
 * 
 * 返回值:
 *    类装饰器返回一个值,它会使用提供的构造函数来替换类的声明。
 * 
 * 注意:
 *    如果你要返回一个新的构造函数,你必须注意处理好原来的原型链。 
 *    在运行时的装饰器调用逻辑中 不会为你做这些。
 */

const myclassDecorator=(target:any)=>{
  //可以用来监视
  console.log('监视',target)

  //可以混入,在构造函数的原型链上混入sayHello方法
  Object.assign(target.prototype,{
    sayHello(){
      console.log('你好',this.name)
    }
  })

  //可以冻结该类及原型
  Object.freeze(target)
  Object.freeze(target.prototype)

  //可以打印属性描述符
  let msg=Object.getOwnPropertyNames(target)
  let msgarr=new Map()

  //不能直接遍历target,因为此处target只是个构造函数
  for(let i of msg){
    msgarr.set(i,Object.getOwnPropertyDescriptor(target,i))
  }
  console.log(msgarr)

  //可以重载但必须是target的子类,不然会类型不兼容
  return class extends target{
    public name:string;
    public age:number=18;

    //构造签名必须与重载前兼容
    constructor(name:string){
      super(name)
    }
    sayName(){
      // super.sayName()
      console.log('我是重载后的类')
    }
  }
}

@myclassDecorator
class Animal{
  constructor(public name:string){}

  sayName(){
    console.log('你好我是',this.name)
  }
}

//
const fish:any=new Animal('tirger')
console.log(fish)
fish.sayName()
fish.sayHello()

/* 监视 [class Animal]
Map(3) {
  'length' => { value: 1, writable: false, enumerable: false, configurable: false },        
  'name' => {
    value: 'Animal',
    writable: false,
    enumerable: false,
    configurable: false
  },
  'prototype' => {
    value: { sayHello: [Function: sayHello] },
    writable: false,
    enumerable: false,
    configurable: false
  }
}
Animal { name: 'tirger', age: 18 }
我是重载后的类
你好 tirger */

2.属性装饰器

属性装饰器声明在一个属性声明之前(紧靠着属性声明)。 属性装饰器不能用在声明文件中(.d.ts),或者任何外部上下文(比如 declare的类)里。

/**
 * 属性装饰器
 * 
 * 调用签名:
 * (target:any,propertyKey:propertyKey)=>void
 * 
 * 参数:
 *    target:any 被修饰属性的类对象
 *    propertyKey:propertyKey 该属性名称
 *    target对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
 * 
 * 注意:
 *    属性描述符不会作为参数传入属性装饰器。
 *    因为目前没有办法在定义一个原型对象的成员时描述一个实例属性,
 *    并且没办法监视或修改一个属性的初始化方法。返回值也会被忽略。
 *    因此,属性描述符只能用来监视类中是否声明了某个名字的属性。
 */

function metaMsg(target:any,propertyKey:PropertyKey){
  // console.log(new target.constructor('yeshifu'))
  console.log(propertyKey)
}

class C{

  @metaMsg
  name:string

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

let test3=new C('今天你好')

console.log(test3.name)

3.方法装饰器

方法装饰器声明在一个方法的声明之前(紧靠着方法声明)。 它会被应用到方法的 属性描述符上,可以用来监视,修改或者替换方法定义。 方法装饰器不能用在声明文件( .d.ts),重载或者任何外部上下文(比如declare的类)中。

/**
 * 方法装饰器
 * 
 * 调用签名:
 *    (target:any,propertyKey:PropertyKey,descriptor:PropertyDescriptor)=>void | TypedPropertyDescriptor<T>
 * 
 * 参数:
 *    target:any 被修饰方法的类对象
 *    propertyKey:PropertyKey 被修饰方法的名称
 *    descriptor:propertyDescriptor 被修饰方法的描述符
 * 
 * 返回值:
 *    如果访问器装饰器返回一个值,它会被用作方法的属性描述符
 *    如果代码输出目标版本小于ES5返回值会被忽略。
 * 
 * 注意:
 *    如果要改变value值,必须与原方法签名兼容
 *    如果代码输出目标版本小于ES5,属性描述符将会是undefined
 * 
 */
function modify(value: boolean):MethodDecorator {
  return function (target: any,propertyKey: PropertyKey,descriptor: PropertyDescriptor) {
    const _self = descriptor.value
    if (value) {
      descriptor.value = function (...arg: string[]) {
        console.log('--->开始')
        _self.call(this, ...arg)
        console.log('<---结束')
      }
    }
    return {
      value: descriptor.value,
      configurable: false,
      enumerable: true,
    }
  }
}

class B {
  constructor(public name = '叶师傅') {}

  @modify(true)
  say(...arg: string[]) {
    console.log(this.name, ...arg)
  }
}

delete B.prototype.say
const test1 = new B('法外狂徒张三')
console.log(test1)
test1.say('666')
/**
 * B { name: '法外狂徒张三' }
 * --->开始
 * 法外狂徒张三 666
 * <---结束
 */

4.访问器装饰器

访问器装饰器声明在一个访问器的声明之前(紧靠着访问器声明)。 访问器装饰器应用于访问器的 属性描述符并且可以用来监视,修改或替换一个访问器的定义。 访问器装饰器不能用在声明文件中(.d.ts),或者任何外部上下文(比如 declare的类)里。

/**
 * 访问器装饰器
 * 
 * 调用签名:
 *    (target:any,propertyKey:PropertyKey,descriptor:PropertyDescriptor)=>void | TypedPropertyDescriptor<T>
 * 
 * 参数:
 *    target:any 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
 *    prototypeName:prototypeKey 成员的名字
 *    descriptor:PrototypeDescriptor 成员的属性描述符
 * 
 * 返回值:
 *    访问器装饰器返回一个值,它会被用作方法的属性描述符。
 *    如果代码输出目标版本小于ES5返回值会被忽略。
 * 
 * 注意  
 *    如果代码输出目标版本小于ES5,Property Descriptor将会是undefined。
 *    TypeScript不允许同时装饰一个成员的get和set访问器。
 *    取而代之的是,一个成员的所有装饰的必须应用在文档顺序的第一个访问器上。
 *    这是因为,在装饰器应用于一个属性描述符时,它联合了get和set访问器,而不是分开声明的。
 */

const getDescriptor=():MethodDecorator=>(target:any,propertyKey:string,descriptor:PropertyDescriptor)=>{
  
  //修改成员属性描述符
  descriptor.configurable=false
  console.log(descriptor)
  descriptor.get=()=>{
    return '我的世界'
  }
}

class D{
  private static _myname='i am is static'

  @getDescriptor()
  get name(){
    return D._myname
  }

  set name(value:string){
    D._myname=value
  }
}
const test12=new D()
console.log(test12.name)
/* {
  get: [Function: get name],
  set: [Function: set name],
  enumerable: false,
  configurable: false
}
我的世界 */


5.参数装饰器

参数装饰器声明在一个参数声明之前(紧靠着参数声明)。 参数装饰器应用于类构造函数或方法声明。 参数装饰器不能用在声明文件(.d.ts),重载或其它外部上下文(比如 declare的类)里。

/**
 * 参数装饰器
 * 
 * 调用签名:
 *    (target:object,propertyKey:PropertyKey,parameterIndex:number)=>void | any
 * 
 * 参数:
 *    target:object 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
 *    propertyKey:PropertyKey 成员名称(这里指修饰符修饰的函数名称)
 *    parameterIndex:number 参数在argunment里的索引
 * 
 * 返回值:
 *    参数装饰器的返回值会被忽略。
 * 
 * 注意:
 *    参数装饰器只能用来监视一个方法的参数是否被传入
 */
function required(target:object,propertyKey:PropertyKey,parameterIndex:number){
  console.log(target)
  console.log(propertyKey)
  console.log(parameterIndex)
  console.log(arguments)
}

 class Greeter {
  greeting: string;

  constructor(message: string) {
      this.greeting = message;
  }

  
  greet(@required name: string) {
      return "Hello " + name + ", " + this.greeting;
  }
}

const test4=new Greeter('我很好')

console.log(test4.greet('叶师傅'))

6.装饰器工厂

装饰器已经介绍完了,装饰器工厂是一个函数,返回值是一个装饰器

/**
 * 装饰器工厂
 * @name()
 * 
 * 注意:
 *    普通装饰器是直接
 *    @name
 *    而装饰器工厂需要在后面再加一个括号
 *    @name()
 * 
 * ts提供了4个装饰器类型:
 *    ClassDecorator 类装饰器
 *    MethodDecorator 方法装饰器 访问器装饰器
 *    PrototypeDecorator 属性装饰器
 *    ParameterDecorator 参数装饰器
 */

function myTestclassD():ClassDecorator{
  return function(target:any){
    Object.assign(target.prototype,{
      sayHello(){
        console.log(`你好我是${this.name}我已经${this.age}岁了`)
      }
    })
  }
}


@myTestclassD()
class ABC{
  constructor(private name:string,public age:number){}
}


const test13:any=new ABC('张三',18)
console.log(test13)
test13.sayHello()
/* ABC { name: '张三', age: 18 }
你好我是张三我已经18岁了 */


装饰器执行顺序

类中不同声明上的装饰器将按以下规定的顺序应用:

  1. 参数装饰器,然后依次是方法装饰器,访问符装饰器,或属性装饰器应用到每个实例成员。
  2. 参数装饰器,然后依次是方法装饰器,访问符装饰器,或属性装饰器应用到每个静态成员。
  3. 参数装饰器应用到构造函数。
  4. 类装饰器应用到类。

总之按照从”小“到”大“的顺序执行

总结

装饰器名称类型参数返回值
类装饰器ClassDecoratortarget可以返回一个新的构造函数
属性装饰器PrototypeDecoratortarget、propertyKeyvoid
方法装饰器MethodDecoratortarget、propertyKey、descriptor可以返回一个方法的属性描述符
访问器装饰器MethodDecoratortarget、propertyKey、descriptor可以返回一个方法的属性描述符
参数装饰器ParameterDecoratortarget、propertyKey、parameterIndexvoid

注意:

  1. 类装饰器返回的构造函数需要与原来的类型兼容,要处理好原型链的关系
  2. 只有方法类型的装饰器能获取成员的属性描述符
  3. 在装饰器调用时处于编译阶段,类还仅仅只是个构造函数,不能访问属性,只能访问原型上的方法
  4. TypeScript不允许同时装饰一个成员的get和set访问器。取而代之的是,一个成员的所有装饰的必须应用在文档顺序的第一个访问器上。 这是因为,在装饰器应用于一个属性描述符时,它联合了get和set访问器,而不是分开声明的

疑惑:

  1. 学了装饰器是不是感觉很鸡肋?没错,我刚开始的时候也这么觉得。
  2. 有些装饰器感觉没有运用场景
  3. 感觉有些功能受限制,比如获取属性的描述符

以上问题是因为还有一位霸王没有出场,那就是Reflect-Metadata。

而且很多的用法都需要去慢慢发现,慢慢积累,笔者目前积累还不够深后,待积累雄厚必发文详细介绍装饰器的运用场景。