# TypeScript装饰器

在 TS 中,装饰器仍然是一项实验性特性,未来可能有所改变,所以如果你要使用装饰器,需要在 tsconfig.json 的编译配置中开启experimentalDecorators,将它设为 true

# 装饰器定义

装饰器是一种新的声明,它能够作用于 类声明方法访问符属性参数 上。使用 @ 符号加一个名字来定义,如 @decorate,这的 decorate 必须是一个函数或者求值后是一个函数,这个 decorate 命名不是写死的,是你自己定义的,这个函数在 运行的时候被调用,被装饰的声明作为 参数 会自动传入。要注意装饰器要紧挨着要修饰的内容的前面,而且所有的装饰器不能用在声明文件(.d.ts)中,和任何外部上下文中。

先定义一个函数,然后这个函数有一个参数,就是要装饰的目标,装饰的作用不同,这个 target 代表的东西也不同。

装饰器是一个函数,给类,方法,属性,参数进行修饰,进行功能扩展。

# 装饰器工厂

可以传递参数。

装饰器工厂也是一个函数,它的返回值是一个函数,返回的函数作为装饰器的调用函数。如果使用装饰器工厂,那么在使用的时候,就要加上函数调用,如下:

function setProp () { // 这是一个装饰器工厂
    return function (target) { //  这是装饰器
        // ...
    }
}

@setProp()

# 装饰器组合

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

  • 书写在同一行上:
@f @g x
  • 书写在多行上:
@f
@g
x

多个装饰器的执行顺序:

  • 装饰器工厂从上到下依次执行,但是只是用于返回函数但不调用函数;
  • 装饰器函数从下到上依次执行,也就是执行工厂函数返回的函数。
function setProp1 () {
    console.log('get setProp1')
    return function (target) {
        console.log('setProp1')
    }
}
function setProp2 () {
    console.log('get setProp2')
    return function (target) {
        console.log('setProp2')
    }
}
@setProp1()
@setProp2()
class Test {}
// 打印出来的内容如下:
/**
 'get setProp1'
 'get setProp2'
 'setProp2'
 'setProp1'
*/

多个装饰器,会先执行装饰器工厂函数获取所有装饰器然后再从后往前执行装饰器的逻辑

# 装饰器求值

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

  • 参数装饰器,方法装饰器,访问符装饰器或属性装饰器应用到每个实例成员;
  • 参数装饰器,方法装饰器,访问符装饰器或属性装饰器应用到每个静态成员;
  • 参数装饰器应用到构造函数;
  • 类装饰器应用到类。

# 类装饰器

类装饰器在类声明之前声明,类装饰器应用于类的声明。

类装饰器应用于类构造函数,可以用来监视,修改或替换类定义。

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

如果类装饰器返回一个值,它会使用提供的构造函数来替换类的声明。

通过装饰器,我们就可以修改类的原型对象和构造函数。

function addName(constructor: any) {
    constructor.prototype.name = "lison";
}
@addName
class A { }
const a = new A();
console.log(a.name); // 类型“A”上不存在属性“name”。

定义类 A 并没有定义属性 name,会报错,可以通过类型断言解决报错。

const a: any = new A();

如果类装饰器返回一个值,那么会使用这个返回的值替换被装饰的类的声明,所以我们可以使用此特性修改类的实现。但是要注意的是,我们需要自己处理原有的原型链。我们可以通过装饰器,来覆盖类里一些操作,来看官方的这个例子:

function classDecorator<T extends { new (...args: any[]): {} }>(target: T) {
  return class extends target {
    newProperty = "new property";
    hello = "override";
  };
}
@classDecorator
class Greeter {
  property = "property";
  hello: string;
  constructor(m: string) {
    this.hello = m;
  }
}
console.log(new Greeter("world"));
/*
{
    hello: "override"
    newProperty: "new property"
    property: "property"
}
*/

首先我们定义了一个装饰器,它返回一个类,这个类继承要修饰的类,所以最后创建的实例不仅包含原 Greeter 类中定义的实例属性,还包含装饰器中定义的实例属性。还有一个点,我们在装饰器里给实例添加的属性,设置的属性值会覆盖被修饰的类里定义的实例属性,所以我们创建实例的时候虽然传入了字符串,但是 hello 还是装饰器里设置的"override"。我们把这个例子改一下:

function classDecorator(target: any): any {
  return class {
    newProperty = "new property";
    hello = "override";
  };
}
@classDecorator
class Greeter {
  property = "property";
  hello: string;
  constructor(m: string) {
    this.hello = m;
  }
}
console.log(new Greeter("world"));
/*
{
    hello: "override"
    newProperty: "new property"
}
*/

在这个例子中,我们装饰器的返回值还是返回一个类,但是这个类不继承被修饰的类了,所以最后打印出来的实例,只包含装饰器中返回的类定义的实例属性,被装饰的类的定义被替换了。

如果我们的类装饰器有返回值,但返回的不是一个构造函数(类),那就会报错了。

# 方法装饰器

方法装饰器用来处理类中方法,它可以处理方法的属性描述符,可以处理方法定义。方法装饰器在运行时也是被当做函数调用,含 3 个参数:

  • 装饰静态成员时是类的构造函数,装饰实例成员时是类的原型对象;
  • 成员的名字;
  • 成员的属性描述符。

来看例子:

function enumerable(bool: boolean) {
  return function(
    target: any,
    propertyName: string,
    descriptor: PropertyDescriptor
  ) {
    console.log(target); // { getAge: f, constructor: f }
    descriptor.enumerable = bool;
  };
}
class Info {
  constructor(public age: number) {}
  @enumerable(false)
  getAge() {
    return this.age;
  }
}
const info = new Info(18);
console.log(info);
// { age: 18 }
for (let propertyName in info) {
  console.log(propertyName);
}
// "age"

这里的 @enumerable(false)是一个 装饰器工厂。 当装饰器 @enumerable(false)被调用时,它会修改属性描述符的 enumerable 属性。

因为这个装饰器修饰在下面使用的时候修饰的是实例(或者实例继承的)的方法,所以装饰器的第一个参数是类的原型对象;第二个参数是这个方法名;第三个参数是这个属性的属性描述符的对象,可以直接通过设置这个对象上包含的属性描述符的值,来控制这个属性的行为。

如果 方法装饰器返回一个值,那么会用这个值作为方法的属性描述符对象

function enumerable(bool: boolean): any {
  return function(
    target: any,
    propertyName: string,
    descriptor: PropertyDescriptor
  ) {
    return {
      value: function() {
        return "not age";
      },
      enumerable: bool
    };
  };
}
class Info {
  constructor(public age: number) {}
  @enumerable(false)
  getAge() {
    return this.age;
  }
}
const info = new Info();
console.log(info.getAge()); // "not age"

我们在这个例子中,在方法装饰器中返回一个对象,对象中包含 value 用来修改方法,enumerable 用来设置可枚举性。我们可以看到最后打印出的 info.getAge()的结果为"not age",说明我们成功使用 function () { return “not age” } 替换了被装饰的方法 getAge () { return this.age }

注意,当构建目标小于 ES5 的时候,方法装饰器的返回值会被忽略。

# 访问器装饰器

访问器装饰器声明在一个访问器的声明之前(紧靠着访问器声明)。访问器也就是 set 和 get 方法,一个在设置属性值的时候触发,一个在获取属性值的时候触发。

首先要注意一点的是,TS 不允许同时装饰一个成员的 get 和 set 访问器,只需要这个成员 get/set 访问器中定义在前面的一个即可。

访问器装饰器也有三个参数,和方法装饰器是一模一样的。来看例子:

function enumerable(bool: boolean) {
  return function(
    target: any,
    propertyName: string,
    descriptor: PropertyDescriptor
  ) {
    descriptor.enumerable = bool;
  };
}
class Info {
  private _name: string;
  constructor(name: string) {
    this._name = name;
  }
  @enumerable(false)
  get name() {
    return this._name;
  }
  @enumerable(false) // error 不能向多个同名的 get/set 访问器应用修饰器
  set name(name) {
    this._name = name;
  }
}

这里我们同时给 name 属性的 set 和 get 访问器使用了装饰器,所以在给定义在后面的 set 访问器使用装饰器时就会报错。经过 enumerable 访问器装饰器的处理后,name 属性变为了不可枚举属性。同样的,如果访问器装饰器有返回值,这个值会被作为属性的属性描述符。

# 属性装饰器

属性装饰器声明在属性声明之前,它有 2 个参数,和方法装饰器的前两个参数是一模一样的。属性装饰器没法操作属性的属性描述符,它只能用来判断某各类中是否声明了某个名字的属性。

function printPropertyName(target: any, propertyName: string) {
  console.log(propertyName);
}
class Info {
  @printPropertyName
  name: string;
  @printPropertyName
  age: number;
}

如果 属性装饰器返回一个值,那么会用这个值作为属性的属性描述符对象

function nameDecorator(target: any, key: string): any{
    const descriptor: PropertyDescriptor = {
        value: "nameDecorator2",
        writable: true,
    }

    return descriptor;
}

class Test {
    @nameDecorator
    name = "test"
}

const a: any = new Test();
a.name = 'nameDecorator'
console.log(a.__proto__.name) // nameDecorator2
console.log(a.name); // nameDecorator

我们在这个例子中,在属性装饰器中返回一个对象,对象中包含 value 用来修改原型上的属性,writable 用来设置可写性。注意 writable 会影响实例上的属性赋值,如果设置为 false 会报错,赋值会报错。Cannot assign to read only property 'name' of object '#<Test>'。同时注意,value是对应原型上的属性,不会影响实例的属性name。

# 参数装饰器

参数装饰器有 3 个参数,前两个和方法装饰器的前两个参数一模一样:

  • 装饰静态成员时是类的构造函数,装饰实例成员时是类的原型对象;
  • 成员的名字;
  • 参数在函数参数列表中的索引。

参数装饰器的返回值会被忽略,来看下面的例子:

function required(target: any, propertName: string, index: number) {
  console.log(`修饰的是${propertName}的第${index + 1}个参数`);
}
class Info {
  name: string = "lison";
  age: number = 18;
  getInfo(prefix: string, @required infoType: string): any {
    return prefix + " " + this[infoType];
  }
}
interface Info {
  [key: string]: string | number | Function;
}
const info = new Info();
info.getInfo("hihi", "age"); // 修饰的是getInfo的第2个参数

这里我们在 getInfo 方法的第二个参数之前使用参数装饰器,从而可以在装饰器中获取到一些信息。

# 装饰器使用案例

const userInfo: any = undefined;

function catchError(msg: string) {
  return function(target: any, key: string, descriptor: PropertyDescriptor) {
    const fn = descriptor.value;
    descriptor.value = function() {
      try {
        fn();
      } catch (e) {
        console.log(msg);
      }
    };
  };
}

class Test {
  @catchError('userInfo.name 不存在')
  getName() {
    return userInfo.name;
  }
  @catchError('userInfo.age 不存在')
  getAge() {
    return userInfo.age;
  }
  @catchError('userInfo.gender 不存在')
  getGender() {
    return userInfo.gender;
  }
}

const test = new Test();
test.getName();
test.getAge();

我们在这个例子中,可以通过 catchError 装饰器,灵活的捕获函数的异常。动态扩展了函数的行为,没有直接注入捕获错误的代码。代码更加优雅。

# 参考

更新时间: 5/13/2020, 11:23:00 PM