接前两篇:
6、类方法装饰器
方法指的是我们加到类上的函数,或者是从超类继承的函数。方法装饰器是这样的
ts
class A {
@MethodDecorator()
fly(meters: number) {
// code
}
}
方法装饰器适用于类的方法,而不适用于函数的参数。
6.1 类方法装饰器
方法装饰器在方法实例化之前被调用。传递给这些函数的参数有
- 对于静态成员,可以是类的构造函数;对于实例成员,可以是类的原型。
- 给出属性名称的字符串
- 成员的
PropertyDescriptor
- 函数
前两个装饰器函数参数与其他几种装饰器类型相同。PropertyDescriptor
与其他一些装饰器类型中使用的对象相同,但使用方式略有不同。JavaScript 对该对象的填写方式与对访问器的填写方式不同。
6.2 类方法装饰器函数
TS 4.x
和 TS 5.x
中方法装饰器函数的参数是不一样的:
TS 4.x
中方法 Decorator 如下所示:
ts
function methodDecorator(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
// ...
};
在 stage1
版本的类方法装饰器中,我们可以获取到三个参数:
target
:被修饰的类name
:类成员的名字descriptor
:属性描述符,对象会将这个参数传给Object.definePropert
想要装饰一个函数,我们必须修改 descriptor.value
,然后再把 descriptor
返回回去,下面例子。
TS 5.x
如下所示:
ts
type ClassMethodDecorator = (
value: Function,
context: {
kind: 'method';
name: string | symbol;
static: boolean;
private: boolean;
access: { get: () => unknown };
addInitializer(initializer: () => void): void;
}
) => Function | void;
发生了很多变化,TS 4.x
和TS 5.x
中的装饰器函数没有 API 兼容性。就我个人而言,我认为TS 5.x
有一个更直观的 interface
。
可以发现相比类装饰器,context
中主要多了三个参数:
static
:是否威静态方法private
:是否为私有方法access
:可以获取到方法的getter
方法(通过它我们就可以公开访问私有方法的字段)
除了方法之外,装饰器也有相应的上下文类型,你也可以使用它们。
请注意,Decorator 函数的返回值可以为 void(即不返回任何内容)或返回新的装饰对象。
返回一个新的装饰对象代替实际的调用。
6.2.1 TypeScript 5 之前的用法
第一步,让我们创建一个装饰器来打印参数值:
ts
import * as util from 'util';
function logMethod(
target: Object,
propertyKey: string,
descriptor: PropertyDescriptor
) {
console.log(`logMethod`, {
target, propertyKey, descriptor,
targetKeys: Object.getOwnPropertyNames(target),
function: descriptor.value,
funcText: descriptor.value.toString()
});
}
class MethodExample {
@logMethod
method(x: number) {
return x * 2;
}
}
这将打印出目标、描述符和描述符中值字段的可用数据。对于 targetKeys
,我们感兴趣的是验证 target 是否是包含方法的类。对于 function,我们了解到值字段包含函数,而使用 toString
可以让我们看到函数的文本。
输出结果:
ts
logMethod {
target: {},
propertyKey: 'method',
descriptor: {
value: [Function: method],
writable: true,
enumerable: false,
configurable: true
},
targetKeys: [ 'constructor', 'method' ],
function: [Function: method],
funcText: 'method(x) {\n return x * 2;\n }'
}
是的,目标显然是包含此方法的类,而值域则是实际函数。
6.2.2 TypeScript 5 之后的用法
有这样一个类:
ts
class OnePieceCharacter {
constructor(private name: string) {}
greet() {
console.log(`${this.name} is saying hello`);
}
}
new OnePieceCharacter('Luffy').greet();
// Luffy is saying hello
通常,如果我们要对 greet
这个方法进行一些调试,你可能会这样写:
ts
greet(){
console.log('LOG: Entering the method');
console.log(`${this.name} is saying hello`);
console.log('LOG: Leaving the method');
}
乍看起来,也没那么复杂,但是如果多个方法都需要调试,我们是不是就会写大量的这种重复代码?这个时候就是方法装饰器的用武之地了!
ts
function logMethod(originalMethod: any, context: any) {
// 定义一个新方法
const replaceMethod = (this: any, ...args: any[]) => {
console.log('Entering the method'); // 扩展方法
const result = originalMethod.call(this, ...args); // 调用原来的方法
console.log('Leaving the method'); // 扩展方法
return result;
}
// 返回装饰后的方法
return replaceMethod;
}
在 replaceMethod
中,我们放入日志语句,然后调用 originalMethod
。重要的是,我们要使用 .call
,并向其传递正确的上下文和参数。
然后,我们就可以在代码中应用装饰器了:
ts
class OnePieceCharacter {
constructor(private name: string) {}
@logMethod
greet() {
console.log(`${this.name} is saying hello`);
}
}
new OnePieceCharacter('Luffy').greet();
// LOG: Entering the method
// Luffy is saying hello
// LOG: Leaving the method
这样,我们就实现了一个可重用的带有代码调试功能的方法装饰器了!是不是很简单!
再进一步,假如我们希望在每个方法的日志都带有一些前缀,以方便我们识别当前打印的是哪个方法,这就需要给方法装饰器传一个唯一的 prefix
了,装饰器工厂就能干这个,请看这个:
ts
type Method = (this: unknown, ...arg: unknown[]) => unknown;
function logWithPrefix(prefix: string) {
return function actulDecorator(
method: Method,
context: ClassMemberDecoratorContext
): Method {
// target 就是当前被装饰的 class 方法
const originalMethod = target;
// 定义一个新方法
const replaceMethod = (this: any, ...args: any[]) => {
console.log(`${prefix}: method start`); // 扩展方法
const result = originalMethod.call(this, args); // 调用原来的方法
console.log(`${prefix}: method end`); // 扩展方法
return result;
}
// 返回装饰后的方法
return replaceMethod;
};
}
在之前实现的装饰器外层再包裹一层函数,然后传入 prefix
,你可以这样使用:
ts
class OnePieceCharacter {
constructor(private name: string) {}
@logMethod('DEBUGGER')
greet() {
console.log(`${this.name} is saying hello`);
}
}
new OnePieceCharacter('Luffy').greet();
// DEBUGGER: Entering the method
// Luffy is saying hello
// DEBUGGER: Leaving the method
1)ClassMethodDecoratorContext
使用装饰器做很多事情,你可以在装饰器中加入自定义逻辑,以调整 this
或 args
。你甚至可以访问装饰函数的上下文。
之前,我们只是用 any 作为装饰函数的参数类型。我们这样做是为了简单起见,但有一些类型可以告诉我们可以访问哪种上下文。
让我们重构一下函数签名:
ts
function logMethod(originalMethod: any, context: ClassMethodDecoratorContext) {}
那么问题来了,什么是ClassMethodDecoratorContext
?官方给出的类型签名如下:
ts
interface ClassMethodDecoratorContext<
This = unknown,
Value extends (this: This, ...args: any) => any = (
this: This,
...args: any
) => any,
> {
/** The kind of class member that was decorated. */
readonly kind: 'method';
/** The name of the decorated class member. */
readonly name: string | symbol;
/** A value indicating whether the class member is a static (`true`) or instance (`false`) member. */
readonly static: boolean;
/** A value indicating whether the class member has a private name. */
readonly private: boolean;
addInitializer(initializer: (this: This) => void): void;
}
2)addInitializer
除了最后一个addInitializer
官方没有给出详细的注释,其他的都有详细的注释说明,那这个addInitializer
又是什么?
addInitializer
函数允许我们提供一个回调函数(类似 hooks
),用来做初始化。那什么时候有用呢?一旦你开始传递函数,它就会变得有用。来看下面的例子:
ts
class OnePieceCharacter {
constructor(private name: string) {}
greet() {
console.log(`${this.name} is saying hello`);
}
}
const luffy = new OnePieceCharacter('Luffy');
luffy.greet();
// Luffy is saying hello
const myFunc = luffy.greet;
myFunc();
// undefined is saying hello
在第二种方法中,"this"
指向的是全局,而不是 OnePieceCharacter
,导致打印出了undefined
。
写一个小装饰器来解决这个问题:
ts
function bound(originalMethod: any, context: ClassMethodDecoratorContext) {
const methodName = context.name;
if (context.private) {
throw new Error('bound can not be used on private methods');
}
context.addInitializer(function () {
this[methodName] = this[methodName].bind(this);
});
}
addInitializer
是通过调用 context.addInitializer(function (){ ... })
添加到类中的,其中 addInitializer
调用中的匿名函数就是将方法绑定到实例的初始化器。
如果我们现在重新运行示例,会得到以下输出结果:
ts
class OnePieceCharacter {
constructor(private name: string) {}
greet() {
console.log(`${this.name} is saying hello`);
}
}
const luffy = new OnePieceCharacter('Luffy');
luffy.greet();
// Luffy is saying hello
const myFunc = luffy.greet;
myFunc();
// Luffy is saying hello
addInitializer
方法会在每次有新的实例被创建,字段被初始化之前被调用,我们可以在这个实际为它绑定this
,然后就可以将方法单独进行调用了~- 需要注意的是:我们虽然用
any
来作为第一个参数的类型,其实是没关系的,因为除了调用原始方法的第一个参数外,我们并没有对它做什么额外的操作。当然,如果你是一个处女座,你也可以创建一个完全类型化的装饰器。
6.3 类方法装饰器的应用
下面使用 TypeScript 5
之前的方法装饰器实现几个常用的装饰器场景。
6.3.1 监听方法调用
因为我们已经为该方法创建了 PropertyDescriptor
,所以我们可以尝试覆盖该函数。就像使用访问器一样,让我们试试新的装饰器,它可以让我们监视函数的输入和输出值。
ts
function MethodSpy(
target: Object,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`MethodSpy before ${propertyKey}`, args);
const result = originalMethod.apply(this, args);
console.log(`MethodSpy after ${propertyKey}`, result);
return result;
}
}
class SpiedOn {
@MethodSpy
area(width: number, height: number) {
return width * height;
}
@MethodSpy
areaCircle(diameter: number) {
return Math.PI * ((diameter / 2) ** 2);
}
}
const spyon = new SpiedOn();
console.log(spyon.area(6, 10));
console.log(spyon.area(16, 20));
console.log(spyon.areaCircle(10));
console.log(spyon.areaCircle(20));
在函数内部,我们保存了 descriptor.value
的原始值,因为这是实际的成员函数。我们将其替换为另一个可接受任意数量参数的函数。请记住,...args
将成为一个数组,其中包含传递给函数的参数。我们首先打印函数名称和提供的参数。然后,我们调用原始方法,提供参数并捕获结果。然后打印函数名和结果,最后返回结果。
然后运行脚本:
ts
MethodSpy before area [ 6, 10 ]
MethodSpy after area 60
60
MethodSpy before area [ 16, 20 ]
MethodSpy after area 320
320
MethodSpy before areaCircle [ 10 ]
MethodSpy after areaCircle 78.53981633974483
78.53981633974483
MethodSpy before areaCircle [ 20 ]
MethodSpy after areaCircle 314.1592653589793
314.1592653589793
在每种情况下,前阶段打印的值是一个数组,后阶段打印的值是每个方法的预期结果。
由于其编写方式,我们可以轻松地将其应用于任何方法的任何装饰器。它甚至能自动获取方法名称。
6.3.2 身份验证
假设我们正在构建一个网络应用程序,该程序要求对某些路由进行身份验证。我们可以创建一个 @auth
装饰器,在允许访问路由之前检查用户是否通过身份验证:
ts
function auth(target: any, key: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
const isAuthenticated = checkIfUserIsAuthenticated();
if (!isAuthenticated) {
// 重定向到登录页或者抛出错误提示
return;
}
originalMethod.apply(this, args);
};
return descriptor;
}
现在,我们可以将 @auth
装饰器应用于任何需要身份验证的方法:
ts
class MyRoutes {
@auth
getProfile() {
// ...
}
@auth
updateProfile(profileData: any) {
// ...
}
}
通过这种设置,@auth
装饰器将在允许访问 getProfile()
和 updateProfile()
方法之前自动检查用户是否已通过身份验证。
6.4 方法和参数装饰器协同工作
我们已经提出,这两种装饰器可以共同产生有用的结果。我们要尝试的概念是参数装饰器,它可以在未提供可选参数的情况下提供默认值。也就是:
ts
class DefaultExample {
@SetDefaults
volume(
z: number,
@ParamDefault<number>(10) x?: number,
@ParamDefault<number>(15) y?: number,
title?: string
) {
const ret = {
x, y, z, volume: x * y * z, title
};
console.log(`volume `, ret);
return ret;
}
}
类,它有一个根据 x、y 和 z 值计算体积的方法。@ParamDefault
装饰器可以让我们指定一个默认值。请注意,参数中的? 表示它是可选的。因此,我们需要一些代码来检测是否存在未提供的参数,并进行替换。
由于 @ParamDefault
是一个参数装饰器,因此除了将其存在记录到数组中外,它不能做任何其他事情,正如我们在学习参数装饰器时所讨论的那样。要替换默认值,我们需要一些针对类实例执行的代码。我们刚刚展示了方法装饰器可以覆盖方法的函数,以便针对类实例执行。我们的计划是注入一个函数,在实际函数之前调用,并使用注入的函数设置默认值。
让我们从 ParamDefault
开始,看看如何实现这一切:
ts
const paramDefaults = [];
...
function ParamDefault<T>(value: T) {
return (target: Object, propertyKey: string | symbol,
parameterIndex: number) => {
paramDefaults.push({
target, propertyKey, parameterIndex, value
});
}
}
This stores data about the parameter that is decorator, as well as the default value to use. The `paramDefaults` array stores this data.
function findDefaults(target: Object, propertyKey: string) {
const ret = [];
for (const def of paramDefaults) {
if (target === def.target && propertyKey === def.propertyKey) {
ret.push(def);
}
}
return ret;
}
function SetDefaults(target: Object, propertyKey: string,
descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`SetDefaults before ${propertyKey}`, args);
for (const def of findDefaults(target, propertyKey)) {
if (typeof args[def.parameterIndex] === 'undefined'
|| args[def.parameterIndex] === null) {
args[def.parameterIndex] = def.value;
}
}
console.log(`SetDefaults after substitution ${propertyKey}`, args);
const result = originalMethod.apply(this, args);
console.log(`SetDefaults after ${propertyKey}`, result);
return result;
}
}
SetDefaults
装饰器函数安装了一个函数,该函数将根据已声明的默认值处理替换值。findDefaults
函数会在 paramDefaults
中搜索与目标和属性键匹配的所有项目。选定默认值后,我们可以查看实际参数中是否有未提供的参数,我们将其定义为未定义或空值。如果没有提供参数,则进行替换,然后调用原始函数并返回结果。
回到我们的例子,有四种组合可以尝试:
- 不提供 x 和 y 值。
- 仅 x 值未提供。
- 只不提供 y 值。
- 同时提供 x 和 y 值。
这样就处理了为缺失参数替换值的所有四种可能性。此外,我们还添加了另一个参数 title
,在这里我们不提供默认值,以验证是否存在缺失。最后,参数 z 作为一个非选项参数,以确保正确处理此类值。
这就是我们的测试代码:
ts
const de = new DefaultExample();
// both x and y missing
console.log(de.volume(10));
console.log('----------------------');
// only x missing
console.log(de.volume(20, null, 20, "Second"));
console.log('----------------------');
// only y missing
console.log(de.volume(30, 30, null));
console.log('----------------------');
// both x and y supplied
console.log(de.volume(40, 40, 50, "Fourth"));
它可以处理刚才提到的四种情况,并确保为每个参数传递一个不同的值,以确保在调用之间不会出现数值失衡。
结果如下:
ts
SetDefaults before volume [ 10 ]
SetDefaults after substitution volume [ 10, 10, 15 ]
volume { x: 10, y: 15, z: 10, volume: 1500, title: undefined }
SetDefaults after volume { x: 10, y: 15, z: 10, volume: 1500, title: undefined }
{ x: 10, y: 15, z: 10, volume: 1500, title: undefined }
----------------------
SetDefaults before volume [ 20, null, 20, 'Second' ]
SetDefaults after substitution volume [ 20, 10, 20, 'Second' ]
volume { x: 10, y: 20, z: 20, volume: 4000, title: 'Second' }
SetDefaults after volume { x: 10, y: 20, z: 20, volume: 4000, title: 'Second' }
{ x: 10, y: 20, z: 20, volume: 4000, title: 'Second' }
----------------------
SetDefaults before volume [ 30, 30, null ]
SetDefaults after substitution volume [ 30, 30, 15 ]
volume { x: 30, y: 15, z: 30, volume: 13500, title: undefined }
SetDefaults after volume { x: 30, y: 15, z: 30, volume: 13500, title: undefined }
{ x: 30, y: 15, z: 30, volume: 13500, title: undefined }
----------------------
SetDefaults before volume [ 40, 40, 50, 'Fourth' ]
SetDefaults after substitution volume [ 40, 40, 50, 'Fourth' ]
volume { x: 40, y: 50, z: 40, volume: 80000, title: 'Fourth' }
SetDefaults after volume { x: 40, y: 50, z: 40, volume: 80000, title: 'Fourth' }
{ x: 40, y: 50, z: 40, volume: 80000, title: 'Fourth' }
虚线是为了读取结果时更加清晰。仔细阅读这些结果,你会发现所有的替换都如预期发生。
6.5 小结
我们通过方法和参数装饰器的组合实现了一个非常有趣的功能,即为缺失的方法参数替换默认值。参数装饰器函数保存了有关默认值的数据,而方法装饰器函数则检测到有默认值的缺失参数。
7、混合装饰器(Hybrid Decorators)
在本文中,我们将讨论 TypeScript 装饰器函数中使用的各种函数签名。通过仔细检查参数,我们可以推断出装饰器所连接的对象类型。这是因为,如果我们查看装饰器函数签名的完整列表,就会发现其中存在一种模式。这样,我们就可以定义类型保护函数来检测使用的是哪种模式,从而可靠地知道装饰器所连接的对象类型。
有了用于测试调用装饰器函数的函数签名的函数,我们就能确定装饰器所连接的对象类型。这样,装饰器就可以用于任何对象类型。本文的目标是提供一种可在所有五种上下文中使用的装饰器:类、属性、访问器、参数和方法。
7.1 TypeScript 装饰器函数方法签名概述
回顾一下本系列其他文章中使用的装饰器函数签名,我们就能得到这些签名:
ts
const accessorfunc = (target: Object,
propertyKey: string,
descriptor: PropertyDescriptor) => {};
const constructorfunc = (constructor: Function) => {};
const methodsfunc = (target: Object, propertyKey: string,
descriptor: PropertyDescriptor) => {};
const parametersfunc = (target: Object,
propertyKey: string | symbol,
parameterIndex: number) => {};
const propertiesfunc = (target: Object, member: string) => {};
例如,class 装饰器只需要一个参数,可以命名为 target。属性装饰器只需要两个参数。其他三个装饰器需要三个参数,它们之间存在差异。参数装饰器的第三个参数是一个数字。访问器和方法装饰器的第三个参数都是 PropertyDescriptor,但构造方法有所不同。
连接到访问器的装饰器接收一组参数,而连接到类的装饰器接收其他参数。每种装饰器类型的装饰器函数都必须能够处理任何一组参数,并能区分一种用途或另一种用途。
这意味着我们需要一个与每种装饰器类型相匹配的通用函数签名。
经过考虑和测试,我们决定使用这种方法来处理每种情况:
ts
(target: Object, propertyKey?: string | symbol, descriptor?: number | PropertyDescriptor)
目标参数对每个参数都是通用的,而其他两个参数则是可选的。另外两个参数的值也有变化。本签名处理所有变量。
这将是一个装饰器函数的函数签名,可用于五种装饰器类型中的任何一种。接下来需要类似类型保护的函数来测试参数。
请记住,在 TypeScript 中,类型保护函数接收一个参数,并测试该参数以确保其属于特定类型。
7.2 混合装饰函数概念的测试用例
创建该类定义是为了测试混合装饰函数是否可行。
ts
@Decorator
class HybridDecorated {
@Decorator
prop1: number;
@Decorator
prop2: string;
@Decorator
method(
@Decorator param1: string,
@Decorator param2: string
) {
console.log(`inside method function`);
return { param1, param2 };
}
#meaning: number = 42;
@Decorator
get meaning() { return this.#meaning; }
set meaning(nm: number) { this.#meaning = nm; }
}
这就需要一个名为 "装饰器" 的函数,它能成功检测出所处的上下文环境,或者换一种说法,即装饰器函数所连接的对象类型。
7.3 原型化装饰器以处理所有 TypeScript 装饰器用途
鉴于前面展示的通用函数签名,必须这样定义装饰器函数:
ts
function Decorator(target: Object,
propertyKey?: string | symbol,
descriptor?: number | PropertyDescriptor) {
console.log(`Decorator target`, target);
console.log(`Decorator propertyKey`, propertyKey);
console.log(`Decorator descriptor`, descriptor);
}
事实上,如果构建一个包含上述类定义和该装饰器实现的文件(例如 first.ts
),就会得到这样的输出结果:
ts
Decorator target {}
Decorator propertyKey prop1
Decorator descriptor undefined
Decorator target {}
Decorator propertyKey prop2
Decorator descriptor undefined
Decorator target {}
Decorator propertyKey method
Decorator descriptor 1
Decorator target {}
Decorator propertyKey method
Decorator descriptor 0
Decorator target {}
Decorator propertyKey method
Decorator descriptor {
value: [Function: method],
writable: true,
enumerable: false,
configurable: true
}
Decorator target {}
Decorator propertyKey meaning
Decorator descriptor {
get: [Function: get meaning],
set: [Function: set meaning],
enumerable: false,
configurable: true
}
Decorator target [class HybridDecorated]
Decorator propertyKey undefined
Decorator descriptor undefined
这证明了这个概念是可行的,TypeScript 装饰器函数可以成功地附加到所有五种可装饰对象类型上。
7.4 测试混合装饰器所连接的对象类型
为了让这些函数更简单一些,我们从下面这个函数开始:
ts
const isset = (val) => {
return typeof val !== 'undefined' && val !== null;
};
const notset = (val) => {
return (typeof val === 'undefined') || (val === null);
};
用于确保参数确实有一个值。如果参数不是未定义的,也不是空值,那么它就有一个值。
ts
export const isClassDecorator = (target: Object,
propertyKey?: string | symbol,
descriptor?: number | PropertyDescriptor) => {
return (isset(target)
&& notset(propertyKey)
&& notset(descriptor));
};
类装饰器的第一个参数只有一个值。
ts
export const isPropertyDecorator = (target: Object,
propertyKey?: string | symbol,
descriptor?: number | PropertyDescriptor) => {
return (isset(target)
&& isset(propertyKey)
&& notset(descriptor));
};
属性装饰器只有前两个参数值。
ts
export const isParameterDecorator = (target: Object,
propertyKey?: string | symbol,
descriptor?: number | PropertyDescriptor) => {
return (isset(target)
&& isset(propertyKey)
&& isset(descriptor)
&& typeof descriptor === 'number');
};
参数装饰器的三个参数都有值,第三个是数字。
ts
export const isMethodDecorator = (target: Object,
propertyKey?: string | symbol,
descriptor?: number | PropertyDescriptor) => {
if ((isset(target)
&& isset(propertyKey)
&& isset(descriptor)
&& typeof descriptor === 'object')) {
const propdesc = <PropertyDescriptor>descriptor;
return (typeof propdesc.value === 'function');
} else {
return false;
}
}
方法装饰器的三个参数都有值,第三个参数是 PropertyDescriptor
对象。该描述符的值域中存储了一个函数。
ts
export const isAccessorDecorator = (target: Object,
propertyKey?: string | symbol,
descriptor?: number | PropertyDescriptor) => {
if ((isset(target)
&& isset(propertyKey)
&& isset(descriptor)
&& typeof descriptor === 'object')) {
const propdesc = <PropertyDescriptor>descriptor;
return (typeof propdesc.value !== 'function')
&& (typeof propdesc.get === 'function'
|| typeof propdesc.set === 'function');
} else {
return false;
}
}
访问器装饰器与方法装饰器类似。区别在于 value 字段没有函数,而 get 和/或 set 字段有函数。
7.5 在混合装饰器函数中使用装饰器类型保护
下面我们来演示如何使用这些函数构建一个装饰器函数,以处理所有五种情况:
ts
import {
isClassDecorator, isPropertyDecorator, isParameterDecorator,
isMethodDecorator, isAccessorDecorator
} from 'decorator-inspectors';
function Decorator(target: Object,
propertyKey?: string | symbol,
descriptor?: number | PropertyDescriptor) {
if (isClassDecorator(target, propertyKey, descriptor)) {
console.log(`Decorator called on class`, target);
} else if (isPropertyDecorator(target, propertyKey, descriptor)) {
console.log(`Decorator called on property ${target} ${String(propertyKey)}`);
} else if (isParameterDecorator(target, propertyKey, descriptor)) {
console.log(`Decorator called on parameter ${target} ${String(propertyKey)} ${descriptor}`);
} else if (isMethodDecorator(target, propertyKey, descriptor)) {
console.log(`Decorator called on method ${target} ${String(propertyKey)}`, descriptor);
} else if (isAccessorDecorator(target, propertyKey, descriptor)) {
console.log(`Decorator called on accessor ${target} ${String(propertyKey)}`, descriptor);
}
else {
console.error(`Decorator called on unknown thing`, target);
console.error(`Decorator called on unknown thing`, propertyKey);
console.error(`Decorator called on unknown thing`, descriptor);
}
}
函数 Decorator
使用通用的 decorator
方法签名。然后,它会使用这些函数来确定在哪种上下文中使用,并打印适当的信息。如果使用不当,则会打印底部的信息。
如果我们在上图所示的示例类中使用该装饰器函数,就会得到以下输出结果:
ts
Decorator called on property [object Object] prop1
Decorator called on property [object Object] prop2
Decorator called on parameter [object Object] method 1
Decorator called on parameter [object Object] method 0
Decorator called on method [object Object] method {
value: [Function: method],
writable: true,
enumerable: false,
configurable: true
}
Decorator called on accessor [object Object] meaning {
get: [Function: get meaning],
set: [Function: set meaning],
enumerable: false,
configurable: true
}
Decorator called on class [class HybridDecorated]
正如您所看到的,所有内容都已正确识别。打印消息的顺序恰好与深入介绍使用和实现 TypeScript 装饰器时讨论的评估顺序一致。
7.6 小结
在查看不同的 TypeScript 装饰器软件包时,我们发现同一个装饰器名称经常被用于多个对象类型。为此,装饰器必须检查其参数,以确定在哪种上下文中使用。这些函数将帮助您在装饰器函数中实现同样的功能。
8、为什么要使用装饰器
通过上面,想必对装饰器有了一个比较深入的认知了,那为什么要使用装饰器呢?总结下来,主要有以下几点:
- 可重用性:装饰器允许你封装特定的行为或功能,并将其应用到多个类、方法、访问器、属性或参数声明中,从而轻松地在代码库中重用相同的功能。
- 抽象:装饰器允许你抽象出复杂的逻辑,并以一种更具声明性的方式将其应用到代码中,从而使代码更易于理解和维护。
- 模块化:装饰器可以将代码分解成更小、更易于管理的部分,从而使测试和调试变得更容易。
- 依赖注入:装饰器广泛应用于 NestJS、Angular 和 Spring 等依赖注入框架。这些框架使用装饰器将依赖注入类的过程自动化。
- 面向切面编程(AOP):装饰器可让您以简洁、有序的方式在代码中添加日志、缓存和安全等跨领域问题。
- 加快开发速度:通过使用装饰器,您可以轻松地实现以下功能,从而加快开发过程