TypeScript装饰器官网笔记

1,176 阅读7分钟
  1. 首先因为装饰器在js中还是提案阶段, 所以在ts中想提前体验装饰器功能的话, 必须在命令行或tsconfig.json 里启用experimentalDecorators

命令行: tsc —target ES5 —experimentalDecorators

tsconfig.json:

{
	"compilerOptions": {
		"target": "ES5",
		"experimentalDecorators": true
	}
}
  1. 装饰器是一种特殊类型的声明, 他能附加到 类、方法、访问符(getter, setter)、属性 或者 参数上.

普通装饰器

function decorator(target: Object) {}

@decorator
name: string;

装饰器工厂

function decorator() {
    return (target: Object) {}
}

@decorator()
name: string;

看到两者之间的区别了吗?

普通装饰器在应用的时候直接是@expression的方式来调用, 并且装饰器使用者无法给装饰器传递任何参数。 而反观装饰器工厂, 在应用的时候是以@expression()的方式来调用, 并且装饰器的定义者可以任意定义参数, 装饰器的使用者也可以传递各种各样的参数, 这样又增加了装饰器的作用.

3.装饰器的求值顺序: 从上到下定义, 从下到上执行, 废话不说, 看代码:

function f() {
    console.log('f(): evaluated');
    return function (target, propertykey, descriptor) {
            console.log('f(): called');
    }
}

function g() {
    console.log('g(): evaluted');
    return function (target, propertyKey, descriptor) {
            console.log('g(): called');
    }
}

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

猜一猜打印的结果是什么?

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

在意料之中是吧~

装饰器的求值或者说执行顺序

  1. 参数装饰器 → 方法装饰器 → 访问符装饰器 | 属性装饰器应用到每个实例成员
  2. 参数装饰器 → 方法装饰器 → 访问符装饰器 | 属性装饰器应用到每个静态成员
  3. 参数装饰器应用到构造函数
  4. 类装饰器应用到类

猜一猜下面的代码执行结果是什么呢?

function classDecorator(target: Object) {
    console.log('class');
}

function methodDecorator(target: Object, propertyKey: string, descriptor: any) {
    console.log('method');
}

function propertyDecorator(target: Object, propertyKey: string) {
    console.log('property');
}

function paramDecorator(target: Object, propertyKey: string, paramIndex: number) {
    console.log('param');
}

@classDecorator
class A {

    @propertyDecorator
    public name: string;

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

    @methodDecorator
    getName(@paramDecorator address: string) {
            return this.name;
    }
}

我相信这里很多人的执行结果应该是param method property class , 我第一次看到这个代码的时候也不例外认为是这个结果, 但是真是的结果往往出乎意料property param method class , 看到这里我相信大部分人都会一头雾水, 上面不是说的执行顺序是先参数装饰器再方法装饰器 再属性装饰器 最后是类装饰器吗?

哈哈哈~ 不着急我们来解释一下

个人猜测ts解析器应该是**从外到里检测** , 什么意思呢? 就是说ts在碰到上面的代码的时候首先是拿着整个类去装饰器求值, 而整个类里面只有3个装饰器, 分别是@classDecorator @propertyDecorator @methodDecorator , 按照上面的求职顺序, 首先是参数装饰器(没有) → 方法装饰器(@methodDecorator) → 属性装饰器(@propertyDecorator) → 类装饰器(@classDecorator) 这TM也不对啊(算了先不管了)~~

后来经过多方求证, 得到结果的时候其实我是非常非常难过的😫~, 应该是ts的文档写错了, 或者说理解有差异, 装饰器除了类装饰器一定是最后执行和参数装饰器一定比方法装饰器早执行之外, 其他的根本没有执行顺序, 你先定义的谁 就执行谁. 嘿嘿😁

再总结一下:

  • 类装饰器一定最后执行
  • 参数装饰器一定在方法装饰器的前面执行
  • 装饰器的优先级按照定义顺序来排序

类装饰器

function ClassDecorator(target: Object): any | void {}
  1. 类装饰器接收一个参数target , 是类本身(不是实例对象)
  2. 只能修改targetprototype属性, 其他的属性是read-only不允许修改
  3. 如果类装饰器返回一个值, 他会使用提供的构造函数来代替类的声明, 看下面的代码
function classDecorator<T extends {new (...args: any[]): {}}>(constructor: T) {
    return class extends constructor {
            name = 'veloma';
            address = '山东省青岛市';
    }
}

@classDecorator
class Greeter {
    age = 20;
    name: string;
    constructor(m: string) {
            this.name = m;
    }

}

console.log(new Greeter('timer')); // { age: 20, name: 'veloma', address: '山东省青岛市' }

在上面的代码中, 我们用classDecorator的返回值来替换了Greeter原来的构造函数, 并且我们可以看到, 在我们自定义的构造函数中所定义的变量, 直接会被添加到this上, 不管这个类中有没有定义这么一个变量.

方法装饰器

function methodDecorator(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<Function>) {}
  1. 方法装饰器有3个参数, 分别是target、propertyKey、descriptor
  2. target: 注意!! 是类的实例对象(如果当前被装饰的方法不是静态方法的话), 如果当前被装饰的方法是静态方法, 那么target就是类本身(不是实例对象)
  3. propertyKey: 方法的名字
  4. descriptor: 方法的描述符(就是Object.defineProperty的第三个参数)

访问器装饰器

这个就不写了, 看官网就可以了, 重复内容.

属性装饰器

function PropertyDecorator(target: Object, propertyKey: string | symbol) {}
  1. 属性装饰器有2 个参数, 分别是target、propertyKey
  2. target: 和方法装饰器一样, 如果是静态属性就是类的构造函数, 如果不是静态属性就是类的原型对象
  3. propertyKey: 属性的名字

参数装饰器

function ParamDecorator(target: Object, propretyKey: string, paramIndex: number) {}
  1. 参数装饰器有3个参数, 分别是target、propertyKey、paramIndex
  2. target: 和上面的属性装饰器相同
  3. propertyKey: 成员的名字, 这里是方法名
  4. paramIndex: 参数的位置

利用方法装饰器和参数装饰器一起来实现一个必传参的装饰器

const requiredMetadataKey = Symbol("required");
const paramsMetadataKey = Symbol('params');

/**
 * @param {Function} target - 目标方法
 * @param {string|Symbol} propertyKey - 方法名
 * @param {number} parameterIndex - 参数位置
 * */
function required (target: Function, propertyKey: string | symbol, parameterIndex: number): void {
  let existingRequiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyKey) || [];
  existingRequiredParameters.push(parameterIndex);  // 把位置push到数组中
  // 给当前的方法设置元数据名为当前的方法名, 值为有@required装饰器的参数位置
  Reflect.defineMetadata(requiredMetadataKey, existingRequiredParameters, target, propertyKey);
}

const validate = (): MethodDecorator =>
  /**
   * @param {Object} target - 目标方法
   * @param {string|Symbol} propertyName - 方法名
   * @param {TypedPropertyDescriptor} descriptor - 方法描述符
   * */
  (target: Object, propertyName: string | symbol, descriptor: TypedPropertyDescriptor<any>) => {
    let method = descriptor.value!;
    descriptor.value = function () {
      let requiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyName); // [1, 0]
      if (requiredParameters) {
        for (let parameterIndex of requiredParameters) {
          if (
            parameterIndex >= arguments.length ||
            arguments[parameterIndex] === undefined ||
            arguments[parameterIndex] === null ||
            !arguments[parameterIndex]
          ) {
            throw new Error(`Missing required argument.`);
          }
        }
      }
      return method.apply(this, arguments);
    }
  }

class Greeter {
  greeting: string;

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

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

const greeter = new Greeter();
greeter('veloma'); // Error: Missing required argument.

看到这里有的小伙伴会问, age本来就限制为number类型, 即使去掉@required也会报错, 是的没错, 但是我们可以给他加上?操作符

来吧greet方法稍微改造一下

@validate()
greet(@required name: string, @required age?: number): string {
    return "hello" + name + ',' + this.greeting;
}

const greeter = new Greeter();
greeter('veloma'); // Error: Missing required argument.

可以看到即使我们给参数类型加上了? 操作符他依旧会报错.

可是这里有一个问题, 当报错的时候, 我们只能看到一句 Missing required argument. 我们可不可以提示是哪个类的哪个方法的哪个参数没填写导致的呢? 嘿嘿😁, 当然可以了.但是解释起来太麻烦了, 不想解释了, 看代码吧.

function required(paramName: string) {
  return (target: Function, propertyKey: string | symbol, parameterIndex: number): void => {
    let existingRequiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyKey) || [];
    existingRequiredParameters.push(parameterIndex);  // 把位置push到数组中
    // 给当前的方法设置元数据名为当前的方法名, 值为有@required装饰器的参数位置
    Reflect.defineMetadata(requiredMetadataKey, existingRequiredParameters, target, propertyKey);
    const params = Reflect.getMetadata(paramsMetadataKey, target, propertyKey) || {};
    params[parameterIndex] = paramName;
    Reflect.defineMetadata(paramsMetadataKey, params, target, propertyKey);
  }
}

// 在参数装饰器之后执行
const validate = (): MethodDecorator =>
  /**
   * @param {Object} target - 目标方法
   * @param {string|Symbol} propertyName - 方法名
   * @param {TypedPropertyDescriptor} descriptor - 方法描述符
   * */
  (target: Object, propertyName: string | symbol, descriptor: TypedPropertyDescriptor<any>) => {
    let method = descriptor.value!;
    descriptor.value = function () {
      const className = target.constructor.name;
      let requiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyName); // [1, 0]
      const params = Reflect.getMetadata(paramsMetadataKey, target, propertyName);
      if (requiredParameters) {
        for (let parameterIndex of requiredParameters) {
          if (
            parameterIndex >= arguments.length ||
            arguments[parameterIndex] === undefined ||
            arguments[parameterIndex] === null ||
            !arguments[parameterIndex]
          ) {
            throw new Error(`Missing required argument -> ${params[parameterIndex]} parameter of method ${propertyName as String} in ${className}.`);
          }
        }
      }
      return method.apply(this, arguments);
    }
  }

class Greeter {
  greeting: string;

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

  @validate()
  greet(@required('name') name: string, @required('age') age?: number): string {
    return "Hello " + name + ", " + this.greeting;
  }
}

const greeter = new Greeter('good evening.');
greeter.greet('veloma'); // Error: Missing required argument -> age parameter of method greet in Greeter.