添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

TypeScript装饰器

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

装饰器定义

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

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

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

装饰器工厂

可以传递参数。

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

1
2
3
4
5
6
7
function setProp () { // 这是一个装饰器工厂
return function (target) { // 这是装饰器
// ...
}
}

@setProp()

装饰器组合

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

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

    多个装饰器的执行顺序:

  • 装饰器工厂从上到下依次执行,但是只是用于返回函数但不调用函数;
  • 装饰器函数从下到上依次执行,也就是执行工厂函数返回的函数。
  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    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'
    */

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

    装饰器求值

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

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

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

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

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

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

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

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

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

    1
    const a: any = new A();

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    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”。我们把这个例子改一下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    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 个参数:

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    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 属性。

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

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    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 访问器中定义在前面的一个即可。

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    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 个参数,和方法装饰器的前两个参数是一模一样的。属性装饰器没法操作属性的属性描述符,它只能用来判断某各类中是否声明了某个名字的属性。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    function printPropertyName(target: any, propertyName: string) {
    console.log(propertyName);
    }
    class Info {
    @printPropertyName
    name: string;
    @printPropertyName
    age: number;
    }

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    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 个参数,前两个和方法装饰器的前两个参数一模一样:

  • 装饰静态成员时是类的构造函数,装饰实例成员时是类的原型对象;
  • 成员的名字;
  • 参数在函数参数列表中的索引。
  • 参数装饰器的返回值会被忽略,来看下面的例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    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 方法的第二个参数之前使用参数装饰器,从而可以在装饰器中获取到一些信息。

    装饰器使用案例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    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 装饰器,灵活的捕获函数的异常。动态扩展了函数的行为,没有直接注入捕获错误的代码。代码更加优雅。

  • 零基础学透 TypeScript
  •