初识TS装饰器

写在最前:本文转自掘金

前言

我们平常开发中或多或少的听说使用过装饰器,也切身感受到它带给我们的遍历。本文将聚焦ts的装饰器,去探讨什么是装饰器,如何使用。

装饰器的演变

  • 2015-3-24
    stage1 阶段,也是目前广为使用的用法,也基本等同ts开启了experimentalDecorators的用法。
  • 2018-09
    进入到stage2阶段,用法和stage1很大不同
  • 2012-12
    针对stage2天进行了一次修改。
  • 2022-03
    正是进入stage3,去掉了matedata部分,使用方式没有太大变化。

js装饰器和ts装饰器

js原生目前不支持装饰器,只能通过babel体验装饰器这个新特性。

装饰器是一种特殊类型的声明,它能够被附加到类声明,方法,访问符,属性或参数上。装饰器使用@expression这种形式,expression求职后必须为一个函数,它会再运行时被调用,被装饰的声明信息作为参数传入。

由于装饰器目前还是实验中的特定,在js中处于stage-3阶段。在ts中已经作为一项实验性予以支持。开启装饰器需要在tsconfig.json文件中启用 experimentalDecorators 编译器选项。

装饰器的使用

类装饰器

类装饰器是我们最常使用到的,它的通常作用是,为该类扩展功能

  • 类装饰器有且只有一个,参数为类的构造函数constructor
  • 如果类装饰器返回一个值,它会使用提供的构造函数来替换类的声明

设想有这样一个场景。目前有一个Tank类,有一个Plane类,有一个Animal类。这三个类都需要一个公共的方法来获取他们所在的位置。我们第一可能想到使用继承来实现。

class BaseClass {
    getPosition() {
        return {
            x: 100,
            y: 200,
            z: 300,
        }
    }
}
class Tank extends BaseClass{}
class Plane extends BaseClass {}
class Animal extends BaseClass {}

这样三个类都可以调用getPosition方法来获取各自的位置了。到目前为止看起来没有什么问题。
现在又有新需求,Tank类和Plane类需要一个新的方法addPetrol来给坦克和飞机加油。而动物不需要加油。此时这种写法好像不能继续进行下去了。而js目前没有直接语法提供多继承的功能,我们的继承方向好像行不通了。这个时候类装饰器可以很完美的实现这样的功能。

装饰器功能之一——能力扩展
我们把getPositionaddPertrol都抽象成一个单独的功能,它们得作用是给宿主扩展对应的功能。

const getPositionDecorator: ClassDecorator = (constructor: Function) => {
    constructor.prototype.getPosition = () => {
        return [100, 200]
    }
}

const addPetrolDecorator: ClassDecorator = (constructor: Function) => {
    constructor.prototype.addPetrol = () => {
        // do something
        console.log(`${constructor.name}进行加油`);
    }
}

@addPetrolDecorator
@getPositionDecorator
class Tank {}
@addPetrolDecorator
@getPositionDecorator
class Plane {}

@getPositionDecorator
class Animal {}

这样的话,假如日后我们有其他的需求,都可以对他进行能力扩展,让其具有加油的能力。
注意,多个装饰器叠加的时候,执行顺序为离被装饰对象越近的装饰器越先执行。

装饰器功能之二——重载构造函数
在类装饰器中如果返回一个值,它会使用提供的构造函数来替换类的声明。

function classDecorator(constructor:T) {
    return class extends constructor {
        newProperty = "new property";
        hello = "override";
    }
}

@classDecorator
class Greeter {
    property = "property";
    hello: string;
    constructor(m: string) {
        this.hello = m;
    }
}

方法装饰器

方法装饰器接收三个参数:

  • 对于静态方法,第一个参数为类的构造函数。对于实例方法,为类的原型对象
  • 第二个参数为方法名。
  • 第三个参数为方法描述符。
  • 方法装饰器可以有返回值,返回值会作为方法的属性描述符

装饰器功能之一——能力增强
我们代码编写时,经常会做一些错误catch,使用装饰器对每个方法进行增加,使它们自动获取catch错误的能力~

const ErrorDecorator: MethodDecorator = (target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) => {
    const sourceMethod = descriptor.value;
    descriptor.value = async function (...args: any) {
        try {
            await sourceMethod.apply(this, args);
        } catch (error) {
            console.error('捕获到了错误');
            // do something
        }
    }
}
class MusicSystem {
    getMusicById(name: string): Promise<{name: string, singer: string}> {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                if (Math.round(Math.random())) {
                    resolve({name: '凤凰传奇', singer: '玲花|曾毅'});
                } else {
                    reject()
                }
            }, 1000);
        })
    }
    
    @ErrorDecorator
    async play(name: string) {
        const music = await this.getMusicById(name);
        // ... do something
        console.log(`在曲库中找到了名为${music.name}的音乐,由${music.singer}进行演唱,敬请欣赏。`);

    }

    @ErrorDecorator
    async deleteByName(name: string) {
        const music = await this.getMusicById(name);
        // ... do something
        console.log(`${music.name}音乐删除成功!`);
    }
}

const musicSystem = new MusicSystem();
musicSystem.play('凤凰传奇');
musicSystem.deleteByName('凤凰传奇');

细心的同学可以发现了,我们在方法装饰器中无法捕获到实际的错误,比如精准报错哪首歌没找到。很遗憾,目前装饰器的原生能力,是无法获取到我们调用时候传入的具体参数的。因为装饰器实在编译阶段执行的。但是,我们可以通过其他方式实现这样的功能,这就是大名鼎鼎的 metadata 。我们会在文章的末尾提到它。

装饰器功能之一——descriptor修改
通过修改descriptor,我们可以实现对方法进行重新描述。比如设置方法禁止修改,禁止删除等。

const DescriptorDecorator: MethodDecorator = (target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) : object => {
    return {
        value: () => {
            console.log('eat方法被替换')
        },
        writable: true,
        enumerable: true,
        configurable: true,
    };
}

class Pig {
    name = 'peiqi';
    @DescriptorDecorator
    eat() {

    }
}

同样的,也可以直接对descriptor进行修改

descriptor.value = () => {console.log('eat方法被替换')};
descriptor.writable = true;
descriptor.enumerable = true;
descriptor.configurable = true;

方法装饰器的使用方式很多,大多数的使用方式是对descriptor的value属性进行替换,拦截等实现功能。

【下边的三个装饰器类型,相对来说使用比较少,有兴趣的小伙伴可以查看原文】

属性装饰器

参数装饰器

访问器装饰器

你可能感兴趣的:(初识TS装饰器)