一.前言
🐻有一个空房子,如果你给这个房子放入了床,它就成了一个休息室,如果你给它放入了一台游戏机,它就变成了一个游戏厅,放入了一个茶几又变成了一个客厅,其实这里不同的家具就是一个装饰器,不同的家具给这个房子扩展了不同的功能,这个概念其实比较容易理解,装饰器字如其名就是装饰的作用,但是当我们使用装饰器的时候有会有很多疑问:为什么?我给函数加了装饰器它就会在调用函数之前输出了装饰器中输出的一些东西?为什么它能将我类中的属性装饰成仅仅可读的状态?其实理解这个问题我们可以简单的理解,就像类每次实例化的时候都会调用构造函数一样,是在构造器底层这么处理的,我们先保证会用,之后可以研究底层到底做了什么,这篇文章会带你从装饰器的使用到底层的操作一网打尽,一次性解决装饰器的疑问。
二.装饰器的种类和理解
🫥有一天祝融
在写代码,共工
过来告诉它,需要增加某个功能,祝融
一看,懵逼了对这个改动会有很多代码的侵入和修改,两人直接大打出手,怒撞不周山,女娲
看到了这一幕觉得这样不利于天下的和平与稳定,于是,念动咒语"让TS中增添一个特性,直接在类和类的成员,属性,参数,方法上面添加@符号,并且在某个地方进行定义一个函数为这个@声明功能"
于是,TS世界中多了一个叫做装饰器的特性,至于为什么能这样写这样用,等你修为更高一点的时候再去领悟吧。
🫥工欲善其事必先利其器,首先我们来搞定一下对装饰器支持的TS环境,我们在终端中输入tsc --init
然后放开下面两项配置,因为装饰器是一种新的特性并没有完全进行支持,其次我们最好安装下ts-node
安装方式非常简单npm install ts-node -g
js
"experimentalDecorators": true,
"emitDecoratorMetadata": true
🤡首先我们来看下装饰器的种类。
- 类装饰器:
target
指的就是这个类本身。
js
function Logger(target: any) {
console.log("Logging...");
console.log(target);
}
@Logger
class MyClass {
// 类实现
}
// 输出内容 :Logging... MyClass
- 方法装饰器:
target
指的是类的原型,key
方法的名称,descriptor
:方法的描述。
js
function Log(target: any, key: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function(...args: any[]) {
console.log("Logging...");
console.log(target);
console.log(key);
console.log(args);
return originalMethod.apply(this, args);
};
}
class MyClass {
@Log
myMethod() {
// 方法实现
}
}
// Logging...
// [object Object] // target:类的原型对象
// myMethod // key:方法名称
// [] // args:方法的参数,这里为空数组
- 属性装饰器:
target
:类的原型对象,key
属性的名称
js
function Readonly(target: any, key: string) {
const descriptor: PropertyDescriptor = {
writable: false
};
Object.defineProperty(target, key, descriptor);
}
class MyClass {
@Readonly
myProperty = 123;
}
- 参数装饰器:
target
:类的原型对象,key
:方法的名称,parameterIndex
:参数的索引
js
function Log(target: any, key: string, parameterIndex: number) {
console.log("Logging...");
console.log(target);
console.log(key);
console.log(parameterIndex);
}
class MyClass {
myMethod(@Log param1: string, @Log param2: number) {
// 方法实现
}
}
// Logging...
// [object Object] // target:类的原型对象
// myMethod // key:方法名称
// 0 // parameterIndex:参数的索引,这里是第一个参数
// Logging...
// [object Object] // target:类的原型对象
// myMethod // key:方法名称
// 1 // parameterIndex:参数的索引,这里是第二个参数
💡Tips:在实际开发中我们一般使用的是框架或者系统提供的装饰器。
👹下面我们来看下他们的加载顺序和执行流程(装饰器是比被装饰对象先执行的)
- 类装饰器的执行顺序是从上到下的,但装饰器的加载顺序是从下到上的。也就是说,最下面的装饰器会最先执行。
- 方法装饰器的执行顺序是从上到下的,但装饰器的加载顺序是从下到上的。也就是说,离方法定义最近的装饰器最先执行。
- 属性装饰器的执行顺序是从上到下的,但装饰器的加载顺序是从下到上的。也就是说,离属性定义最近的装饰器最先执行。
- 参数装饰器的执行顺序是从左到右的,但装饰器的加载顺序是从右到左的。也就是说,参数从左到右依次执行对应的装饰器函数,但装饰器函数的加载顺序是从右到左的。
三.打破对装饰器的幻想
🤡刚看到装饰器是不是觉得很神奇,其实没啥神奇的,我们也可以看成类似于class
的语法糖,装饰器就是如下这种操作的语法糖,只不过是机器代替了手动的创建方式,如下(以类装饰器为例)
js
const MoveDecorator: ClassDecorator = (constructor: Function) => {
constructor.prototype.name = 'zpj'
constructor.prototype.getPosition = (): { x: number, y: number } => {
return { x: 100, y: 100 }
}
}
class Tank {
constructor() {
console.log('构造函数');
}
}
MoveDecorator(Tank);
const tank = new Tank()
console.log(tank.getPosition());
四.装饰器工厂
🦝上面我们学习了装饰器的基本使用,我们知道装饰器本质上就是一个函数,但是我们如果单独写一个函数的话装饰器功能会比较固定和单一,我们可以使用工厂模式来根据不同的内容来返回不同的装饰器。
js
const MusicDecorator = (type: string): ClassDecorator => {
switch (type) {
case 'player':
return (constructor: Function) => {
constructor.prototype.playMusic = (): void => {
console.log(`播放【海阔天空】音乐`);
}
}
break;
default:
return (constructor: Function) => {
constructor.prototype.playMusic = (): void => {
console.log(`播放【aaa】音乐`);
}
}
}
}
@MusicDecorator('tank')
class Tank {
constructor() {
}
}
const tank = new Tank();
(<any>tank).playMusic();
@MusicDecorator('player')
class Player {
}
const xj: Player = new Player();
(xj as any).playMusic()
五.总结
🐻这篇文章主要学习了装饰器相关的内容,装饰器是一个新的特性,但是在Angular
中和Nest
中已经广泛使用了,装饰器并不是一个特别神奇的特性,它的本质是一个函数,我们可以通过现实的例子来理解和使用它,总之装饰器的使用在未来的开发中是一个必然的趋势。