Typesrcipt 装饰器的分类及使用
本文包含的内容:装饰器的分类、装饰器与装饰器工厂、装饰器的执行顺序以及两个装饰器的应用
装饰器的分类
类装饰器
类型声明:
type ClassDecorator = <TFunction extends Function>
(target: TFunction) => TFunction | void;
类装饰器只有一个参数:target
,也就是类的构造器。
如果类装饰器有返回值,那么它将被用来替代原有的类构造器的声明,因此类装饰器适用于继承一个现有类并添加一些属性和方法。
type Consturctor = { new (...args: any[]): any };
function toString<T extends Consturctor>(BaseClass: T) {
console.log(BaseClass === C) // true
return class extends BaseClass {
public num = 77
toString() {
return JSON.stringify(this);
}
};
}
@toString
class C {
public foo = "foo";
public num = 24;
}
console.log(new C().toString()) // {"foo":"foo","num":77}
但装饰器存在一个缺陷:没有类型保护,也就是说:
declare function Blah<T>(target: T): T & {foo: number}
@Blah
class Foo {
bar() {
return this.foo; // Property 'foo' does not exist on type 'Foo'
}
}
new Foo().foo; // Property 'foo' does not exist on type 'Foo'
这是一个TypeScript的已知的缺陷。 临时解决方案是额外提供一个类用于提供类型信息:
declare function Blah<T>(target: T): T & {foo: number}
class Base {
foo: number;
}
@Blah
class Foo extends Base {
bar() {
return this.foo;
}
}
new Foo().foo;
属性装饰器
类型声明:
type PropertyDecorator =
(target: Object, key: string | symbol) => void;
属性装饰器有两个参数:
target
当装饰静态成员时,它是类的构造器;当装饰实例成员时,它是类的原型对象key
被装饰的属性名
属性装饰器没有返回值,即使有也会被忽略。
function decorateAttr(target: any, key: string) {
console.log(target === A)
console.log(target === A.prototype)
console.log(key)
}
class A {
@decorateAttr // 输出 true false staticAttr
static staticAttr: any
@decorateAttr // 输出 false true instanceAttr
instanceAttr: any
}
除了收集信息外,还可以用属性装饰器来给类添加额外的方法和属性:
function capitalizeFirstLetter(str: string) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
function observable(target: any, key: string): any {
// prop -> onPropChange
const targetKey = "on" + capitalizeFirstLetter(key) + "Change";
target[targetKey] =
function (fn: (prev: any, next: any) => void) {
let prev = this[key];
Reflect.defineProperty(this, key, {
set(next) {
fn(prev, next);
prev = next;
}
})
};
}
class C {
@observable
foo = -1;
@observable
bar = "bar";
}
const c = new C();
c.onFooChange((prev, next) => console.log(`prev: ${prev}, next: ${next}`))
c.onBarChange((prev, next) => console.log(`prev: ${prev}, next: ${next}`))
c.foo = 100; // prev: -1, next: 100
c.foo = -3.14; // prev: 100, next: -3.14
c.bar = "baz"; // prev: bar, next: baz
c.bar = "sing"; // prev: baz, next: sing
方法装饰器
类型声明:
type MethodDecorator = <T>(
target: Object,
key: string | symbol,
descriptor: TypedPropertyDescriptor<T>
) => TypedPropertyDescriptor<T> | void;
// tips
interface TypedPropertyDescriptor<T> {
enumerable?: boolean;
configurable?: boolean;
writable?: boolean;
value?: T;
get?: () => T;
set?: (value: T) => void;
}
方法装饰器有三个参数:
target
当装饰静态成员时,它是类的构造器;当装饰实例成员时,它是类的原型对象key
被装饰的属性名descriptor
属性的描述器,即Object.getOwnPropertyDescriptor(target,key)
如果方式装饰器有返回值,那么它将被用来替代属性的描述器(descriptor)。
我们可以通过 descriptor
参数改变原本方法的实现,添加一些共用逻辑:
function logger(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = function (...args) {
console.log('params: ', ...args);
const result = original.call(this, ...args);
console.log('result: ', result);
return result;
}
}
class C {
@logger
add(x: number, y:number ) {
return x + y;
}
}
const c = new C();
c.add(1, 2); // params: 1, 2 // result: 3
function decorateMethod(target: any, key: string, descriptor: PropertyDescriptor){
return{
value: function(...args: any[]){
const result = descriptor.value.apply(this, args) * 2;
return result;
}
}
}
class A {
@decorateMethod
sum(x: number,y: number){
return x + y
}
}
console.log(new A().sum(1,2)) // 6
访问器装饰器
访问器装饰器和方法装饰器很接近,唯一的区别在于描述器(descriptor)中的某些 key 不同:
方法装饰器的描述器的key为:
value
writable
enumerable
configurable
访问器装饰器的描述器的key为:
get
set
enumerable
configurable
通过访问器装饰器我们可以将某个属性设为不可变值:
function immutable(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const original = descriptor.set;
descriptor.set = function (value: any) {
return original.call(this, { ...value })
}
}
class C {
private _point = { x: 0, y: 0 }
@immutable
set point(value: { x: number, y: number }) {
this._point = value;
}
get point() {
return this._point;
}
}
const c = new C();
const point = { x: 1, y: 1 }
c.point = point;
console.log(c.point) // { "x": 1, "y": 1 }
console.log(c.point === point) // false
或者让某个属性不可更改:
function configurable (target: any, key: string, descriptor: PropertyDescriptor) {
return {
writable: false
}
};
class A {
_age = 18
@configurable
get age(){
return this._age
}
set age(num: number){
this._age = num
}
}
const a = new A()
a.age = 20 // TypeError: Cannot assign to read only property 'age'
参数装饰器
类型声明:
type ParameterDecorator = (
target: Object,
key: string | symbol,
parameterIndex: number
) => void;
参数装饰器有三个参数:
target
当装饰静态成员时,它是类的构造器;当装饰实例成员时,它是类的原型对象key
被装饰的属性名**(是方法名而不是参数名)**parameterIndex
参数在方法中所处位置的下标
参数装饰器没有返回值,即使有也会被忽略。
单独的参数装饰器能做的事情很有限,它一般都被用于记录可被其它装饰器使用的信息。
装饰器与装饰器工厂
普通装饰器
interface Person {
name: string
age: string
}
function classDecorator(target: any) {
target.prototype.name = 'sechi'
target.prototype.age = '23'
}
@classDecorator
class Person {
constructor() { }
}
const p = new Person()
console.log(p.name) // sechi
console.log(p.age) // 23
装饰器工厂
不同类型的装饰器本身的参数是固定的,当我们需要自定义装饰器参数时(也就是说需要给装饰器传参),便可以构造一个装饰器工厂函数:
interface Person {
name: string
age: string
}
function classDecorator(name: string, age: number) {
return function(target: any) {
target.prototype.name = name
target.prototype.age = age
}
}
@classDecorator('genji', 30)
class Person {
constructor() { }
}
const p = new Person()
console.log(p.name) // genji
console.log(p.age) // 30
装饰器的执行顺序
执行时机
装饰器只在解释执行时应用一次:
function f(C) {
console.log('apply decorator')
return C
}
@f
class A {}
// 输出 apply decorator
上面代码中我们并没有去构造 A
的实例,但装饰器函数依旧运行了一次。
执行顺序
装饰器工厂函数从上至下开始执行,装饰器函数从下至上开始执行。可以把这个执行顺序简单理解为一个洋葱模型。
function first() {
console.log("first(): factory evaluated");
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("first(): called");
};
}
function second() {
console.log("second(): factory evaluated");
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("second(): called");
};
}
class ExampleClass {
@first()
@second()
method() {}
}
// first(): factory evaluated
// second(): factory evaluated
// second(): called
// first(): called
上面只是一个粗略的执行顺序,实际上不同类型的装饰器执行次序也不同。
- 实例成员:参数/方法/属性装饰器(三种装饰器没有先后顺序,谁先声明就先执行谁)
- 静态成员:参数/方法/属性装饰器(三种装饰器没有先后顺序,谁先声明就先执行谁)
- 构造器:参数装饰器
- 类装饰器
function f(key: string): any {
console.log("evaluate: ", key);
return function () {
console.log("call: ", key);
};
}
@f("Class Decorator")
class C {
@f("Static Property")
static prop?: number;
@f("Static Method")
static method(@f("Static Method Parameter") foo) {}
constructor(@f("Constructor Parameter") foo) {}
@f("Instance Method")
method(@f("Instance Method Parameter") foo) {}
@f("Instance Property")
prop?: number;
}
打印信息:
evaluate: Instance Method
evaluate: Instance Method Parameter
call: Instance Method Parameter
call: Instance Method
evaluate: Instance Property
call: Instance Property
evaluate: Static Property
call: Static Property
evaluate: Static Method
evaluate: Static Method Parameter
call: Static Method Parameter
call: Static Method
evaluate: Class Decorator
evaluate: Constructor Parameter
call: Constructor Parameter
call: Class Decorator
例子
一、运行时检查参数类型
如果我们想给接口添加运行时检查类型的能力,可以通过结合使用不同的装饰器来达成这一目的,总的来说分为两个步骤:
- 使用参数装饰器来标记需要检查的参数
- 使用方法装饰器来改变方法的
descriptor
的值,使得方法执行前先运行参数检查器,如果检查不通过就抛出异常,如果通过了就正常执行方法。
type Validator = (x: any) => boolean;
// 存储每个方法的参数检查器
const validateMap: Record<string, Validator[]> = {};
// 1. 标记需要检查的参数
function typedDecoratorFactory(validator: Validator): ParameterDecorator {
return (_, key, index) => {
const target = validateMap[key as string] ?? [];
target[index] = validator;
validateMap[key as string] = target;
}
}
function validate(_: Object, key: string, descriptor: PropertyDescriptor) {
const originalFn = descriptor.value;
descriptor.value = function(...args: any[]) {
// 2. 运行检查器
const validatorList = validateMap[key];
if (validatorList) {
args.forEach((arg, index) => {
const validator = validatorList[index];
if (!validator) return;
const result = validator(arg);
if (!result) {
throw new Error(
`Failed for parameter: ${arg} of the index: ${index}`
);
}
});
}
// 3. 运行原有的方法
return originalFn.call(this, ...args);
}
}
const isInt = typedDecoratorFactory((x) => Number.isInteger(x));
const isString = typedDecoratorFactory((x) => typeof x === 'string');
class C {
@validate
sayRepeat(@isString word: string, @isInt x: number) {
return Array(x).fill(word).join('');
}
}
const c = new C();
c.sayRepeat('hello', 2); // pass
c.sayRepeat('', 'lol' as any); // throw an error
二、统一的try/catch
一般来讲,我们都需要写一个 try/catch
来包裹 async/await
函数以便抛出错误,但通过方法装饰器我们可以消除繁琐的 try/catch
来统一抛错。
function log (target: any, key: string, descriptor: PropertyDescriptor){
return {
value: async function (...args) {
try{
await descriptor.value.apply(this, args)
}catch(e){
console.log(e)
}
}
};
};
class A {
@log
asyncHandle(){
return Promise.reject('Async Error')
}
}
new A().asyncHandle() // Async Error
参考文章:
转载自:https://juejin.cn/post/7232130225869832253