• home > webfront > ECMAS > typescript >

    Typescript装饰器Decorators浅析

    Author:zhoulujun Date:

    装饰器是一种特殊类型的声明,它能够被附加到类声明,方法, 访问符,属性或参数上。就是在原有代码外层包装了一层处理逻辑。可以简单地理解为是非侵入式的行为修改。

    先了解下装饰器模式与元编程

    装饰器模式(Decorator)

    装饰器模式又名包装(Wrapper)模式。装饰器模式以对客户端透明的方式拓展对象的功能,是继承关系的一种替代方案。

    装饰器模式以对客户透明的方式动态的给一个对象附加上更多的责任。换言之,客户端并不会觉得对象在装饰前和装饰后有什么不同。装饰器模式可以在不是用创造更多子类的情况下,将对象的功能加以拓展。

    具体参看《再谈设计模式—模式23种设计模式总结

    元编程(MetaProgramming)

    元编程是用代码在编译期或运行期生成/改变代码。

    Meta- 这个前缀在希腊语中的本意是「在…后,越过…的」,类似于拉丁语的 post-,大陆将 meta- 这个前缀译为「元」并不恰当。台湾译为「后设」,稍微好一点点,但仍旧无法望文生义。也许「自相关」是个不错的选择,「自相关数据」、「自相关语言」、「自相关编程」——但是好像又太罗嗦了。

    元编程通常有两种方式起作用。一种方式是通过应用程序接口(API)来暴露运行时引擎的内部信息。另一种方法是动态执行包含编程命令的字符串。因此,“程序能编写程序”。虽然两种方法都能用,但大多数方法主要靠其中一种。

    ECMAScript 2015 开始,JavaScript 获得了 Proxy 和 Reflect 对象的支持,允许你拦截并定义基本语言操作的自定义行为(例如,属性查找,赋值,枚举,函数调用等)。借助这两个对象,你可以在 JavaScript 元级别进行编程。

    TypeScript的装饰器,是个非常好的选择

    TypeScript装饰器

    装饰器(Decorators)为我们在类的声明及成员上通过元编程语法添加标注提供了一种方式。 Javascript里的装饰器目前处在 建议征集的第二阶段,但在TypeScript里已做为一项实验性特性予以支持。

    装饰器定义

    装饰器是一种特殊类型的声明,它能够被附加到类声明,方法, 访问符,属性或参数上。 

    装饰器使用 @expression这种形式,expression求值后必须为一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入。


    通俗的理解可以认为就是在原有代码外层包装了一层处理逻辑

    个人认为装饰器是一种解决方案,而并非是狭义的@Decorator,后者仅仅是一个语法糖罢了。

    装饰器在身边的例子随处可见,一个简单的例子

    水龙头上边的起泡器就是一个装饰器,在装上以后就会把空气混入水流中,掺杂很多泡泡在水里。

    但是起泡器安装与否对水龙头本身并没有什么影响,即使拆掉起泡器,也会照样工作,水龙头的作用在于阀门的控制,至于水中掺不掺杂气泡则不是水龙头需要关心的。


    为什么要用装饰器

    可能有些时候,我们会对传入参数的类型判断、对返回值的排序、过滤,对函数添加节流、防抖或其他的功能性代码,基于多个类的继承,各种各样的与函数逻辑本身无关的、重复性的代码。

    所以,对于装饰器,可以简单地理解为是非侵入式的行为修改

    如何定义装饰器

    装饰器本身其实就是一个函数,理论上忽略参数的话,任何函数都可以当做装饰器使用。

    //target指向的是HelloWordClass
    function helloWord(target: any) {
        console.log('hello Word!');
    }
    
    @helloWord
    class HelloWordClass {
    
    }

    装饰器组合

    多个装饰器可以同时应用到一个声明上 @f @g x

    装饰器类型

    装饰器的类型有:类装饰器、访问器装饰器、属性装饰器、方法装饰器、参数装饰器,但是没有函数装饰器(function)。

    类装饰器

    应用于类构造函数,其参数是类的构造函数。


    function addAge(args: number) {    
        return function (target: Function) {
        target.prototype.age = args;
    };
    }
    @addAge(18)
    class Hello {
        name: string;
        age: number;
        constructor() {
            console.log('hello');
            this.name = 'yugo';
        }
    }
    console.log(Hello.prototype.age);//18
    let hello = new Hello();
    console.log(hello.age);//18

    注意class并不是像Java那种强类型语言中的类,而是JavaScript构造函数的语法糖

    方法装饰器

    它会被应用到方法的 属性描述符上,可以用来监视,修改或者替换方法定义。

    方法装饰会在运行时传入下列3个参数:

    1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。

    2. 成员的名字。

    3. 成员的属性描述符{value: any, writable: boolean, enumerable: boolean, configurable: boolean}。

    function addAge(constructor: Function) {
      constructor.prototype.age = 18;
    }
    
    function method(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
       console.log(target);
       console.log("prop " + propertyKey);
       console.log("desc " + JSON.stringify(descriptor) + "\n\n");
    };
    
    @addAge
    class Hello{
      name: string;
      age: number;
      constructor() {
        console.log('hello');
        this.name = 'yugo';
      }
    
      @method
      hello(){
        return 'instance method';
      }
    
      @method
      static shello(){
        return 'static method';
      }
    }

    假如是 shello 这个静态方法,则

    第一个参数是构造器 constructor。

    第二个参数分别是属性名,第三个参数是属性修饰对象。

    访问器装饰器

    访问器装饰器应用于访问器的属性描述符,可用于观察,修改或替换访问者的定义。 访问器装饰器不能在声明文件中使用,也不能在任何其他环境上下文中使用(例如在声明类中)。

    注意: TypeScript不允许为单个成员装饰get和set访问器。相反,该成员的所有装饰器必须应用于按文档顺序指定的第一个访问器。这是因为装饰器适用于属性描述符,它结合了get和set访问器,而不是单独的每个声明。

    访问器装饰器表达式会在运行时当作函数被调用,传入下列3个参数:

    1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。

    2. 成员的名字。

    3. 成员的属性描述符。

    注意  

    • 如果代码输出目标版本小于ES5,Property Descriptor将会是undefined。

    • 如果代码输出目标版本小于ES5返回值会被忽略。

     class Point {
        private _x: number;
        private _y: number;
        constructor(x: number, y: number) {
            this._x = x;
            this._y = y;
        }
    
        @configurable(false)
        get x() { return this._x; }
    
        @configurable(false)
        get y() { return this._y; }
    }
    function configurable(value: boolean) {
        return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
            descriptor.configurable = value;
        };
    }



    方法参数装饰器

    参数装饰器表达式会在运行时当作函数被调用

    传入下列3个参数:

    1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。

    2. 参数的名字。

    3. 参数在函数参数列表中的索引。

    const parseConf = [];
    class Modal {
        @parseFunc
        public addOne(@parse('number') num) {
            console.log('num:', num);
            return num + 1;
        }
    }
    
    // 在函数调用前执行格式化操作
    function parseFunc(target, name, descriptor) {
        const originalMethod = descriptor.value;
        descriptor.value = function (...args: any[]) {
            for (let index = 0; index < parseConf.length; index++) {
                const type = parseConf[index];
                console.log(type);
                switch (type) {
                    case 'number':
                        args[index] = Number(args[index]);
                        break;
                    case 'string':
                        args[index] = String(args[index]);
                        break;
                    case 'boolean':
                        args[index] = String(args[index]) === 'true';
                        break;
                }
                return originalMethod.apply(this, args);
            }
        };
        return descriptor;
    }
    
    // 向全局对象中添加对应的格式化信息
    function parse(type) {
        return function (target, name, index) {
            parseConf[index] = type;
            console.log('parseConf[index]:', type);
        };
    }
    let modal = new Modal();
    console.log(modal.addOne('10')); // 11



    属性装饰器


    属性装饰器表达式会在运行时当作函数被调用,传入下列2个参数:

    1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。

    2. 成员的名字。

    function log(target: any, propertyKey: string) {
        let value = target[propertyKey];
        // 用来替换的getter
        const getter = function () {
            console.log(`Getter for ${propertyKey} returned ${value}`);
            return value;
        }
        // 用来替换的setter
        const setter = function (newVal) {
            console.log(`Set ${propertyKey} to ${newVal}`);
            value = newVal;
        };
        // 替换属性,先删除原先的属性,再重新定义属性
        if (delete this[propertyKey]) {
            Object.defineProperty(target, propertyKey, {
                get: getter,
                set: setter,
                enumerable: true,
                configurable: true
            });
        }}
    class Calculator {
        @log
        public num: number;
        square() {
            return this.num * this.num;
        }}
    let cal = new Calculator();
    cal.num = 2;
    console.log(cal.square());
    // Set num to 2
    // Getter for num returned 2
    // Getter for num returned 2
    // 4



    装饰器加载顺序

    装饰器可以同时使用多个因此需要知道其执行顺序

    • 不同类型装饰器执行顺序:属性>方法>方法参数>类

    • 在多个同类型的装饰器中的执行顺序 => 从后向前执行


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

    • 参数装饰器,然后依次是方法装饰器,访问符装饰器,或属性装饰器应用到每个实例成员。

    • 参数装饰器,然后依次是方法装饰器,访问符装饰器,或属性装筋器应用到每个静态成员。

    • 参数装纺器应用到构造函数。

    从上述例子得出如下结论:

    1. 有多个参数装饰器时:从最后一个参数依次向前执行

    2. 方法和方法参数中参数装饰器先执行。

    3. 类装饰器总是最后执行。

    4. 方法和属性装饰器,谁在前面谁先执行。因为参数属于方法一部分,所以参数会一直紧紧挨着方法执行。


    参考文章:

    TypeScript装饰器(decorators) https://www.jianshu.com/p/afef44d449bd

    装饰器 https://www.tslang.cn/docs/handbook/decorators.html

    Typescript装饰器浅析 https://segmentfault.com/a/1190000019081501



    转载本站文章《Typescript装饰器Decorators浅析》,
    请注明出处:https://www.zhoulujun.cn/html/webfront/ECMAScript/typescript/2020_0413_8382.html