ts支持装饰器,装饰器是一种语法糖,本质就是一个函数。一个类加上一个装饰器,可以看作是调用一个函数并把该类作为参数传入,对该类进行修改后又返回另一个类,这跟高阶组件的思路差不多。当然,这个思路也不是高阶组件首创的。
先看一个例子
通过代码注释理解代码意图
// 我写了一个类
class Car {
}
// 我想给类添加一个run方法
// 所以写个RunDecorator函数来给类加run方法
// 该方法参数类型T是构造函数类型的写法
// 类的本质就是构造函数
const RunDecorator = <T extends new (...args: any[]) => any>(
constructor: T,
) => {
// 拿到传进来的构造函数,通过继承的方式重载它
// 本质上就是产生了另一个新的类
return class extends constructor {
// 添加run方法
run() {
console.log('runing');
}
}
}
// 把RunDecorator作用到Car上,产生新的类
const NewCar = RunDecorator(Car);
// 用新的类创建实例
const newCar = new NewCar();
// 新的类实例执行run方法
newCar.run();
上面的代码的执行结果就仅仅是打印出runing,如下:
runing
上面的RunDecorator,所添加的run方法,固定打印'runing'字符串,但我们希望打印的内容可以灵活指定,并且可以用在不同的类上面,所以我们写一个工厂函数来生成不同输出的RunDecorator,这个工厂函数本质上就是利用闭包特性,传入不同的内容,产生不同输出的RunDecorator:
// 返回RunDecorator的工厂方法
// 参数是某个字符串
const CreateRunDecorator = (who: string) => {
// 返回RunDecorator方法
return <T extends new (...args: any[]) => any>(
constructor: T,
) => {
// RunDecorator方法依然会重载某个构造函数或类
return class extends constructor {
// 新的run方法,根据工厂函数调用时传入的不同字符串,决定输出的不同内容
run() {
console.log(`${who} runing`);
}
}
}
}
class Car {
}
// 给Car类添加的run方法输出内容会带有'Car'字符串
const NewCar = CreateRunDecorator('Car')(Car);
class Dog {
}
// 给Dog类添加的run方法输出内容会带有'Dog'字符串
const NewDog = CreateRunDecorator('Dog')(Dog);
// 新类创建新实例并各自调用run方法
const newCar = new NewCar();
newCar.run();
const newDog = new NewDog();
newDog.run();
输出结果如下:
Car runing
Dog runing
用装饰器
为了方便以后使用,我把CreateRunDecorator单独放在一个文件(decorator.ts)里面,并对外导出:
// decorator.ts
export const CreateRunDecorator = (who: string) => {
return <T extends new (...args: any[]) => any>(
constructor: T,
) => {
return class extends constructor {
run() {
console.log(`${who} runing`);
}
}
}
}
哪里想使用就引入进来
// 引入
import { CreateRunDecorator } from './decorator';
class Car {
}
const NewCar = CreateRunDecorator('Car')(Car);
const newCar = new NewCar();
newCar.run();
为了给某个类添加run方法,每次都要调用CreateRunDecorator产生新的类,写法不够简洁,利用typescript的装饰器支持,就可以直接给一个类赋能,不需要产生新类,写法简洁,当然这都是语法糖,本质上跟我们前面的调用行为类似,首先要在配置文件tsconfig.json上打开装饰器配置:
{
"compilerOptions": {
......
"experimentalDecorators": true,
......
}
......
}
然后就可以用CreateRunDecorator来装饰一个类,注意,给一个类添加了类声明所没有的方法,调用新方法时类型判断会报错,需要加上一行类型声明防止报错:
import { CreateRunDecorator } from './decorator';
@CreateRunDecorator('Car')
class Car {
// 装饰器添加了新方法,但Car不包含该新方法,下面调用run时,
// typescript的类型检测就会报错,这里加上这个类型声明就不会报错,
// 实际编译成js后不存在这些代码,完全是写给类型检测工具看的
[key: string]: any;
}
const car = new Car();
car.run();
ts装饰器的使用,是一种语法糖,本质上就是调用装饰器返回的方法,以Car为参数,返回的新类再覆盖原来的Car。
修改类中的方法
上面通过装饰器,给类添加了新的方法。现在我想修改类中已有的方法,还是举个例子:
// 有一个类
class Car {
// 里面有个run方法,打印一句话
run() {
console.log('Car running');
}
}
// 有个修改方法,修改目标里面的某个方法
// 第一个参数是目标对象,第二个参数是属性名
function logSome(target: any, propertyName: string) {
// 拿到目标的方法的描述符
const descriptor = Object.getOwnPropertyDescriptor(target, propertyName);
// 描述符的value就是方法本身
const method = descriptor?.value;
// 重新修改方法属性
Object.defineProperty(target, propertyName, {
...descriptor, // 其他描述属性沿用原来的
// 覆盖原来的value值,创造新的方法
value: function(...args: any[]) {
// 多了一行打印
console.log(`method ${propertyName} begin run`);
// 继续调用原来的方法
return method.apply(this, args);
}
});
}
// 类方法不写在构造函数,而是写在原型上
logSome(Car.prototype, 'run');
(new Car()).run();
运行结果:
method run begin run
Car running
接下来改成用装饰器实现同样的修改。
装饰器就是一个方法,ts会调用装饰器这个方法,传入一些参数,对于方法装饰器,ts会传入三个参数:
- 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
- 成员的名字,即方法名。
- 成员的属性描述符,即上面通过
Object.getOwnPropertyDescriptor获取到的对象。
这里解释下第一个参数,上面我们调用logSome时传的第一个参数不是类本身,而是类的原型,因为run方法是实例成员,它存放在类的原型中,需要修改类的原型属性才能修改这个方法,实际上,对类的方法使用装饰器时,ts会判断该方法是静态成员还是实例成员,如果是静态成员,ts会直接传入构造函数,对于实例成员,会传入类的原型。说白了,ts会帮我们判断,传进来的目标肯定就是方法所在的对象。
而传给装饰器的第三个参数就是方法属性的描述符,也省了我们自己获取,把装饰器方法放到decorator.ts文件中导出。
要注意的是,跟上面不使用装饰器的修改不同的是,使用装饰器修改方法可以直接修改方法的描述符的value即可,不需要通过Object.defineProperty修改目标属性,因为,ts会拿传进去的描述符对象(propertyDescriptor)来调用Object.defineProperty修改run属性,你也可以在装饰器方法最后返回一个对象,ts会用它来作为run属性的最新描述符:
// decorator.ts
export function logSome(
target: any,
propertyName: string,
propertyDescriptor: PropertyDescriptor,
) {
// 先存起方法本身
const method = propertyDescriptor?.value;
// 直接修改方法属性描述符
propertyDescriptor.value = function(...args: any[]) {
console.log(`method ${propertyName} begin run`);
return method.apply(this, args);
}
// 返回描述符
// ts处理装饰器最后会用返回的描述符覆盖原来方法属性的描述符
return propertyDescriptor;
}
具体使用:
import { logSome } from './decorator';
class Car {
@logSome
run() {
console.log('Car running');
}
}
(new Car()).run();
中间总结
装饰器可以作用于类的各个部位,它本质上就是一个函数。用装饰器装饰不同部位时,传给装饰器的参数也有所不同,会根据具体部位传入充足的信息。所以我们只要先了解清楚各个部位的装饰器会传什么参数,再根据参数信息思考如何实现我们想要的效果。
了解其它装饰器
上面描述了类和方法装饰器,接着再简单看看其它装饰器。
访问器装饰器
访问器指get方法和set方法,比如:
class User {
private _age: number;
@fix
get age() {
return this._age;
}
}
访问器装饰器被调用时,传进去的参数按顺序有这些:
- 类的构造函数或者是类的原型,跟方法装饰器一样,取决于装饰的是静态属性还是实例成员;
- 属性名,比如上面例子中是
age; - 成员的属性描述器;
注意:同一个属性的get和set方法,只能给其中一个加装饰器,不能同时给两个都加,因为装饰器应用于的属性描述符,是结合了get和set访问器,而不是各自单独的描述符。
属性装饰器
例子:
class User {
@fix
age: number;
}
属性装饰器运行时会收到的参数是:
- 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象;
- 成员属性名。
参数装饰器
例子:
class User {
eat(@need str: string) {
console.log(str);
}
}
参数装饰器运行时会收到的参数是:
- 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象;
- 成员属性名;
- 参数下标,即第几个参数,从0开始。
装饰器执行顺序
多种类型装饰器的执行顺序,可以说是从内到外,
参数->方法->get/set/属性->类装饰器
同一个属性/方法可以加多个装饰器,比如:
class User {
@log
@fix
eat(str: string) {
console.log(str);
}
}
多个装饰器,先调用离属性近的,比如上例,会先调用fix,执行完后,返回结果再继续调用log。 其他类型的装饰器类似。
反射和元数据
Reflect这个对象本身集合了很多对象相关的操作,,比如:
const obj = {
name: 'kkk',
};
// 读取对象属性值
console.log(Reflect.get(obj, 'name'));
// 获取对象所有key
console.log(Reflect.ownKeys(obj));
// 获取对象属性描述符
console.log(Reflect.getOwnPropertyDescriptor(obj, 'name'));
打印结果:
kkk
[ 'name' ]
{ value: 'kkk', writable: true, enumerable: true, configurable: true }
新的提案提出给Reflect添加更多操作元数据的方法。
元数据,可以理解为类或对象的辅助信息,或者就单纯认为就是信息。属性的描述符信息也可以认为是元信息,数组长度也可以认为是一种元信息。
因为给Reflect添加新的方法的提案一直没有落实,所以原生不能支持,但有牛人实现了库来实现那些新方法。
安装该库:
npm install reflect-metadata --save
现在可以通过新的方法给类添加元数据,例子:
// 需要引入库
import 'reflect-metadata';
const metaKey = 'eat-key';
const metaValue = {speed: 'fast'};
const classMetaKey = 'fix-key';
const classMetaValue = {view: 'far'};
// 用装饰器给类添加元数据
@Reflect.metadata(classMetaKey, classMetaValue)
class Someone {
// 用装饰器给类实例方法添加元数据
@Reflect.metadata(metaKey, metaValue)
eat() {
console.log('eating');
}
}
class User {
eat() {
console.log('eating');
}
}
// 用方法而不是装饰器给类实例方法添加元数据,等价于用装饰器
Reflect.defineMetadata(metaKey, metaValue, User.prototype, 'eat');
// 用方法而不是装饰器给类添加元数据
Reflect.defineMetadata(classMetaKey, classMetaValue, User);
// 给实例方法添加的元素数只能到实例或原型获取
console.log(Reflect.getMetadata(metaKey, new User(), 'eat'));
console.log(Reflect.getMetadata(metaKey, User.prototype, 'eat'));
console.log(Reflect.getMetadata(metaKey, new Someone(), 'eat'));
console.log(Reflect.getMetadata(metaKey, Someone.prototype, 'eat'));
console.log(Reflect.getMetadata(classMetaKey, User));
console.log(Reflect.getMetadata(classMetaKey, Someone));
输出结果:
{ speed: 'fast' }
{ speed: 'fast' }
{ speed: 'fast' }
{ speed: 'fast' }
{ view: 'far' }
{ view: 'far' }
反射控制元数据方法配合装饰器
当给类实例方法添加装饰器时,我想知道方法的参数类型是什么,比如:
class User {
@log
eat(str1: string, str2: string) {
console.log(str1, str2);
}
}
我的log方法想根据参数类型打印不同日志,我可以给这个方法再加一个装饰器,装饰器会添加方法的参数类型:
import 'reflect-metadata';
function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
// 通过反射方法获取元数据
const typeL = Reflect.getMetadata('design:paramtypes', target, propertyKey);
// 根据参数类型打印不同日志
if (typeL[0] === String) {
console.log('eat method need string params');
} else {
console.log('eat method need not string params');
}
}
class User {
// 在log之前先加一个元数据,记录方法的参数类型
@log
@Reflect.metadata('design:paramtypes', [String, String])
eat(str1: string, str2: string) {
console.log(str1, str2);
}
}
在我们实现一些装饰器时,可能需要额外的辅助元数据,比如实例方法的装饰器逻辑可能需要知道方法参数的类型,返回值类型;比如实现属性装饰器时,想知道属性的类型;等等。
typescript提供了另一个特性,该特性在我们添加装饰器时,会帮我们针对装饰器目标插入更早执行的元数据,让我们能方便拿到一些有用的元数据。
在typescript的配置文件打开该特性:
{
"compilerOptions": {
......
"experimentalDecorators": true,
"emitDecoratorMetadata": true, // 打开这个
......
}
}
打开这个配置之后,为了看看效果,我再写一个例子,复用之前用过的一些装饰器:
import { logSome, CreateRunDecorator, logAccess } from './decorator';
@CreateRunDecorator('shit')
class Car {
_seatCount = 5;
@logSome
run() {
console.log('Car running');
}
@logAccess
get seat() {
return this._seatCount;
}
}
主要是为了看编译成js后的代码:
它给run方法和seat访问器都添加了额外的元数据,并且元数据的key就是固定那几个:
"design:type"
"design:paramtypes"
"design:returntype"
那我们实现run方法和seat访问器的装饰器时,就可以通过Reflect.getMetadata,使用这几个固定的key作为第一个参数,就可以获取到方法的类型,参数类型,返回值类型。