当前位置 博文首页 > jcLee95的博客:TypeScript笔记(15)—— 深入理解TypeScript中

    jcLee95的博客:TypeScript笔记(15)—— 深入理解TypeScript中

    作者:[db:作者] 时间:2021-09-18 15:52

    TypeScript笔记(15):TypeScript装饰器

    CSDN:jcLee95
    邮箱:291148484@163.com

    专题目录:https://blog.csdn.net/qq_28550263/article/details/115601920

    如果你对TypeScript装饰器有所疑惑,那么欢迎阅读本文。希望对你有所帮助,感谢支持!


    【导读】:和其它很多功能一样,装饰器也不是TypeScript独有。实际上装饰器是一种用于拓展、修改对象行为的一种软件涉及模式。这种模式提供了我们不用知道原对象就可以对其进行对其更改的特点。

    1. 类 装饰器

    • 用于对类进行声明:本质上是对类的构造进行包装(修改)。我们 可以为同一个类定义多个装饰器,通过@decorator_name的形式写在类声明之前。
    • 参数:类的构造函数作为其唯一的参数。

    定义类装饰器的基本格式为:

    function classDec1(constructor) {
    }
    

    使用类装饰器的格式为将@装饰器名紧写在被装饰的类上,后面其它装饰器多数类似(除参数装饰器写在形参前)。即装饰器一般这样应用:

    @decorator_name
    class{
      ...
    }
    

    示例1. 多个类装饰器装饰于同一个类的执行顺序:

    function classDec1(constructor: Function) {
       /**
        * 类装饰器1:对类装饰类的constructor做一些事情
        * @param {constructor}:Function       被装饰类的构造函数作为其唯一参数
        */
       console.log('执行类装饰器`classDec1`的语句');
       console.log('类装饰器语句执行:可在构造类前对类构造函数进行包装');
    }
    
    function classDec2(constructor: Function) {
       /**
        * 类装饰器2:对类装饰类的constructor做一些事情
        * @param {constructor}:Function       被装饰类的构造函数作为其唯一参数
        */
       console.log('执行类装饰器`classDec2`的语句');
    }
    
    @classDec1
    @classDec2
    class MyClass {
     attrib: string;
     constructor(attrib: string) {
       console.log('原构造函数执行');
       this.attrib = attrib;
     }
    }
    
    let cls = new MyClass("attrib value")
    

    Out[]:

    执行类装饰器`classDec2`的语句
    执行类装饰器`classDec1`的语句
    类装饰器语句执行:可在构造类前对类构造函数进行包装
    原构造函数执行
    

    示例2. 使用类装饰器对类扩展
    定义一个类装饰器

    // 如何 扩展/装饰 一个类
    function classDec<T extends {new(...args:any[]):{}}> (constructor:T) {
       /**
        * 接收被装饰类的构造函数`constructor`作为唯一参数
        * 剩余参数 `...args` 接收原被装饰类构造器的参数
        */
       return class extends constructor {
          /* 对被装饰的类进行扩展 */
           newProperty1 = "new property 1";   // 被扩展的新属性1
           newProperty2 = "new property 2";   // 被扩展的新属性2
       }
    }
    

    其中

    • 要扩展被装饰类的属性就需要接收以前的属性。被装饰类的构造函数仍作为唯一参数传入其类型注释为泛型,应是从之前继承而来的。剩余参数 ...args是一个数组参数(...args:any[])被装饰类构造时的参数列表。
    • 实质上{new(...args:any[]):{}}是所谓mixin 构造类型,它是有一个构造签名、带有一个类型为any[]的剩余参数(用…表示的数组型参数)和一个对象(object-like)的 返回类型。比如给定一个对象类型X,那么new (...args: any[]) => X是一个带有实例类型Xmixin 构造器类型mixin类是扩展类型参数类型表达式的类声明或表达式。
    • 更多参见Mixins与Mix-in类型

    将装饰器应用到类上:

    @classDec                                 // 将写好的装饰器应用到某个类上,如法格式为 @expression
    class MyClass1 {
       property1 = "property 1";
       property2: string;
       constructor(property2: string) {
           this.property2 = property2;
       }
    }
    
    @classDec                                  // 就像被声明的函数一样,同一个装饰器也能反复装饰不同的类
    class MyClass2 {
       constructor() {
          /**这个类在被装饰前啥也没有 */
       }
    }
    
    console.log(new MyClass1("property2 value"));
    console.log(new MyClass2());
    

    Out[]:

    MyClass1 {
      property1: 'property 1',
      property2: 'property2 value',
      newProperty1: 'new property 1',
      newProperty2: 'new property 2'
    }
    MyClass2 {
      newProperty1: 'new property 1',
      newProperty2: 'new property 2'
    }
    

    示例3. 在装饰器中提前创建类的实例该实例:

    虽然这个例子比较无聊,但是还是有助于对类装饰器用法的理解的。

    let father = '小明父亲'
    let mother = '小明母亲'
    let child = '小明'
    // 定义一个装饰器
    function assign(constructor:any){
       let a = Object.assign(constructor)   // 通过被装饰类的构造器拷贝该类
       new a(mother,child)
    }
    
    @assign
    class MyClass {
       constructor(parent: string, child: string) {
           console.log(parent+'属于'+child+'的parents。')
       }
    }
    
    new MyClass(father,child)
    

    Out[]:

    小明母亲属于小明的parents。
    小明父亲属于小明的parents。
    

    示例4. 使用装饰器让类封闭。

    // 定义一个装饰器
    function sealed(constructor: Function) {
       Object.seal(constructor);
    }
    
    @sealed
    class MyClass {
       prop1 :string
       prop2: string
       constructor(prop1,prop2) {
          this.prop1 = prop1;
          this.prop2 = prop2;
       }
    }
    
    console.log('MyClass是否是封闭的:'+Object.isSealed(MyClass));
    
    Object.defineProperty(MyClass,'prop1',{
       enumerable:true
    })
    
    for(let i in MyClass){
       console.log(i);
    }
    

    当把@sealed注释掉时,运行的结果为:

    MyClass是否是封闭的:false
    prop1
    

    可以看到在没有使用该装饰器时我们可以为MyClass类(也是对象)的属性使用属性描述符enumerable使之可遍历。然而使用@sealed装饰器后,语句Object.seal(constructor)通过作用类的构造函数装饰类,实际上就是使得类对应的对象封闭。这时如果再对其属性进行描述(参考JavaScript属性描述符相关内容)就会报错:

    通过这样一个装饰器函数,我们只要将其装饰到任意需要封闭的类上,再不改变或重写该类就可以直接将其封闭了。从以上的例子我们看出:

    • 装饰器其实就是且必须是一个函数;
    • 使用装饰器作用到被装饰者上时,即装饰器表达式@expression,其中的expression求值后就是装饰器的函数名;
    • 装饰器在作用到被装饰者时,@expression本质上看就是装饰器函数的调用。

    这里必须指出类本身就是一个对象,typescript使用并扩展ES6类语法糖。实际上在ES6后也不存在基于类的面向对象编程语言中的类,ES的以后类仍然是基于原型链的,而这些类不过是函数模拟的。底层使用装饰器装饰该类前后的JavaScript代码分别为:

    使用装饰器前对应的JavaScript代码

    var MyClass = /** @class */ (function () {
        function MyClass(prop1, prop2) {
            this.prop1 = prop1;
            this.prop2 = prop2;
        }
        return MyClass;
    }());
    

    使用装饰器后对应的JavaScript代码

    var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
        var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
        if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
        else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
        return c > 3 && r && Object.defineProperty(target, key, r), r;
    };
    
    // 装饰器函数
    function sealed(constructor) {
        Object.seal(constructor);
    }
    
    // 被装饰器装饰的类 MyClass 在底层也是函数
    var MyClass = /** @class */ (function () {
        function MyClass(prop1, prop2) {
            this.prop1 = prop1;
            this.prop2 = prop2;
        }
        MyClass = __decorate(
          [sealed],            // 所有通过`@expression`等表达式装收到类的装饰器依顺序在此数组中
          MyClass
        );
        return MyClass;
    }());
    

    2. 方法 装饰器

    • 应用到方法的 属性描述符上;
    • 用来监视,修改或者替换方法定义;
    • 接收三个参数,具体见定义格式部分;
    • 返回值:(如果有)如果方法装饰器返回一个值,它会被用作方法的属性描述符

    Typescript中也可以定义类方法的装饰器,其语法格式为:

    function methodDecorator(target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
       /**
        * 方法装饰器:
        * 类型:MethodDecorator
        * @param {target}       对于实例成员是类的`原型对象`,对于静态成员来说是类的`构造函数 constructor`
        * @param {propertyKey}  成员的名字
        * @param {descriptor}   成员的属性描述符( PropertyDescriptor )
        */
    };
    

    上面说到,如果方法装饰器返回一个值,它会被用作方法的属性描述符(如果代码输出目标版本小于ES5返回值会被忽略),这里简单回顾一下 属性描述符