写在最前:本文转自掘金
前言
我们平常开发中或多或少的听说使用过装饰器,也切身感受到它带给我们的遍历。本文将聚焦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目前没有直接语法提供多继承的功能,我们的继承方向好像行不通了。这个时候类装饰器可以很完美的实现这样的功能。
装饰器功能之一——能力扩展
我们把getPosition
和addPertrol
都抽象成一个单独的功能,它们得作用是给宿主扩展对应的功能。
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属性进行替换,拦截等实现功能。
【下边的三个装饰器类型,相对来说使用比较少,有兴趣的小伙伴可以查看原文】