5. TypeScript 装饰器
5.1 简介
-
定义:是个特殊函数。它能给类、属性、方法、参数加新功能,还能让代码变得更简洁,减少重复工作
-
手动开启:
experimentalDecorators
-
五种类型:
-
类装饰器
-
属性装饰器
-
方法饰器
-
访问器装饰器
-
参数装饰器
-
5.2 类装饰器
-
类装饰器:一种特殊的函数,它可以 “装饰”(也就是应用到)一个类上。当你定义一个类并使用装饰器时,这个装饰器函数会在类定义完成后立即执行,让你有机会为这个类添加一些额外的功能或者修改它的行为
-
语法:用
@装饰器名
写在类的上面
5.2.1 基本语法
- 类装饰器的基本形式是一个函数,它接受一个参数,这个参数就是被装饰的类本身
// 定义一个装饰器函数
function Demo(target: Function) {
// target 就是被装饰的类,这里我们简单地打印它
console.log("被装饰的类:", target);
// 你可以在这里为类添加额外的属性或方法
target.prototype.newMethod = function() {
console.log("这是装饰器添加的新方法");
}
}
// 使用装饰器
@Demo
class Person {
constructor(public name: string) {}
sayHello() {
console.log(`你好,我是 ${this.name}`);
}
}
// 创建Person实例
const person = new Person("张三");
person.sayHello(); // 输出: 你好,我是 张三
// 调用装饰器添加的新方法
// @ts-ignore 因为TypeScript可能不知道这个新方法
person.newMethod(); // 输出: 这是装饰器添加的新方法
-
代码执行流程:
代码加载阶段: 1. 定义装饰器函数 Demo 2. 定义 Person 类 └─ 触发装饰器 Demo(Person) ├─ 打印被装饰的类 └─ 添加 newMethod 到 Person.prototype 代码运行阶段: 3. 创建 person 实例 └─ person 的原型包含 newMethod 4. 调用 person.sayHello() └─ 使用原始方法 5. 调用 person.newMethod() └─ 使用装饰器添加的方法
5.2.2 应用举例
// 定义装饰器
function AddSayHi<T extends { new (...args: any[]): {} }>(target: T) {
// 直接修改原型,添加新方法
target.prototype.sayHi = function() {
console.log(`Hi! I'm ${this.name}`);
};
}
// 使用装饰器
@AddSayHi
class Person {
constructor(public name: string) {}
}
// 创建实例
const person = new Person("小明");
// 调用装饰器添加的方法
person.sayHi(); // 输出: Hi! I'm 小明
代码解释:代码使用 TypeScript 装饰器为类添加了额外功能。首先定义了装饰器AddSayHi
,它接收一个构造函数作为参数,并在其原型上添加了sayHi
方法,该方法会打印包含实例name
属性的问候语。然后将此装饰器应用于Person
类,该类的构造函数接收name
参数并将其设为公有属性。创建Person
实例person
后,就能调用装饰器添加的sayHi
方法,输出Hi! I'm 小明
5.2.3 关于返回值
5.2.3.1 类装饰器有返回值
当类装饰器返回一个新的类时,这个新类会完全替换掉被装饰的原始类。也就是说,当你使用这个装饰器后,原来的类就不存在了,取而代之的是装饰器返回的新类
function replaceClass() {
return class {
sayHello() {
console.log('我是新类的方法');
}
};
}
@replaceClass
class OriginalClass {
sayHello() {
console.log('我是原始类的方法');
}
}
const instance = new OriginalClass();
instance.sayHello(); // 输出:我是新类的方法
代码解释:replaceClass
装饰器返回了一个新的类,当我们创建OriginalClass
的实例并调用sayHello
方法时,实际上调用的是新类中的方法,这说明原始类已经被替换了
5.2.3.2 类装饰器无返回值
如果类装饰器没有返回值(也就是返回undefined
),那么被装饰的类不会被替换,仍然保持原样。装饰器可以在这种情况下修改类的原型或属性,但不会替换整个类
function modifyPrototype(target: Function) {
target.prototype.sayGoodbye = function() {
console.log('再见!');
};
// 没有返回值
}
@modifyPrototype
class MyClass {
sayHello() {
console.log('你好!');
}
}
const obj = new MyClass();
obj.sayHello(); // 输出:你好!
obj.sayGoodbye(); // 输出:再见!
代码解释:modifyPrototype
装饰器没有返回值,它只是在类的原型上添加了一个新方法sayGoodbye
。当我们创建MyClass
的实例时,既可以调用原始类中的sayHello
方法,也可以调用装饰器添加的sayGoodbye
方法,这说明原始类并没有被替换,只是被修改了
5.2.4 关于构造类型
5.2.4.1 仅声明构造类型
借助new
关键字来定义一个单纯的构造类型,这种类型不包含任何静态属性
// 定义一个基础的构造类型,它代表的是可以接受任意数量和类型参数,并且返回一个非空对象的构造函数
type Constructor = new (...args: any[]) => {};
// 这里有一个接收构造类型参数的函数
function test(constructor: Constructor) {
// 我们可以在这里使用这个构造函数,比如创建它的实例
return new constructor();
}
// 定义一个简单的类
class Person {
constructor(public name: string) {}
}
// 把Person类作为参数传递给test函数,这是可行的,因为Person类符合Constructor类型的定义
test(Person);
// 尝试把箭头函数作为参数传递给test函数,会报错,因为箭头函数不能被实例化
// test(() => {}); // 错误:类型 '() => {}' 缺少构造签名
5.2.4.2 声明构造类型并指定静态属性
可以在定义构造类型时,同时为其指定静态属性
// 定义一个构造类型,这个类型不仅可以被实例化,还必须包含一个string类型的静态属性wife
type ConstructorWithWife = {
new (...args: any[]): {}; // 构造签名
wife: string; // 静态属性
};
// 接收特定构造类型的函数
function test(constructor: ConstructorWithWife) {
console.log(constructor.wife); // 可以访问静态属性
return new constructor(); // 也可以创建实例
}
// 定义一个符合上述构造类型的类
class Person {
static wife = "Alice"; // 必须包含静态属性wife
constructor(public name: string) {}
}
// 可以正常工作
test(Person);
// 定义一个缺少静态属性的类
class Animal {
constructor(public name: string) {}
}
// 会报错,因为Animal类没有静态属性wife
// test(Animal); // 错误:类型 'typeof Animal' 中缺少属性 'wife'
5.2.5 替换被装饰的类
-
概念:装饰器可以返回一个新的类(通常是原类的子类),这个新类会完全替换原来的类
-
作用:
-
添加新的属性或方法
-
修改现有方法的行为
-
添加元数据或日志功能
-
-
代码实现:为类添加一个
createdTime
属性,记录实例的创建时间,添加一个getTime()
方法,用于获取创建时间
// 定义装饰器函数
function LogTime<T extends new (...args: any[]) => {}>(target: T) {
// 返回一个新的类,继承自原类
return class extends target {
// 添加新属性:记录创建时间
createdTime: Date;
constructor(...args: any[]) {
// 调用原类的构造函数
super(...args);
// 记录实例创建时间
this.createdTime = new Date();
}
// 添加新方法:获取创建时间
getTime() {
return `该对象创建时间为:${this.createdTime}`;
}
};
}
// 使用装饰器
@LogTime
class User {
constructor(
public name: string,
public age: number
) {}
speak() {
console.log(`${this.name}说:你好啊!`);
}
}
// 创建实例并测试
const user1 = new User('张三', 13);
user1.speak(); // 张三说:你好啊!
console.log(user1.getTime()); // 该对象创建时间为:[具体时间]
- 代码从前面开始运行,运行到
@LogTime
的时候,开始把User
这个类传到LogTime
这个函数里面,而且User
必须符合构造函数类型,也就是User
要是一个可实例化的类,并且把User传给target,在LogTime
装饰器函数内部,返回了一个新的类,这个新类继承自原User
类。这里使用了类装饰器的模式,通过继承扩展原类的功能
注意:
装饰器只能用于类,不能用于函数或变量
装饰器在类定义时执行,而不是实例化时
返回的新类必须继承自原类,以保留原有功能
5.3 装饰器工厂
-
定义:装饰器工厂是个返回装饰器函数的函数,它最大的作用是能让装饰器接收参数
-
例子:定义一个名为
LogInfo
的类装饰器工厂,它的任务是让Person
类的实例能够调用introduce
方法。而且这个方法输出内容的次数,由LogInfo
接收的参数来决定
// 定义一个装饰器工厂 LogInfo,它接受一个参数 n,返回一个类装饰器
function LogInfo(n: number) {
// 装饰器函数,target 是被装饰的类
return function (target: Function) {
// 给被装饰的类的原型添加 introduce 方法
target.prototype.introduce = function () {
// 循环 n 次输出信息
for (let i = 0; i < n; i++) {
console.log(`我的名字:${this.name},我的年龄:${this.age}`);
}
};
};
}
// 使用 LogInfo 装饰器工厂,传入参数 5
@LogInfo(5)
class Person {
constructor(
public name: string,
public age: number
) { }
speak() {
console.log('你好呀!');
}
}
// 创建 Person 实例
let p1 = new Person('张三', 18);
// 调用原有的 speak 方法
p1.speak();
// 调用装饰器添加的 introduce 方法
p1.introduce();
-
流程:
-
定义阶段:定义装饰器工厂,它返回一个装饰器函数。使用
@LogInfo(5)
语法应用装饰器 -
类定义时:调用装饰器工厂,传入参数,装饰器工厂返回装饰器函数后立即传入被装饰的类然后装饰器函数修改类的原型。
-
实例化时:创建类的实例,此时类已经被装饰器修改。实例可以使用装饰器添加的方法。
-
注意:装饰器在类定义时(使用
class
关键字来定义一个类的时候)执行,而不是在实例化时(使用new
关键字来创建类的实例的时候)执行
5.4 装饰器组合
-
区分:
-
装饰器:是一个直接作用于目标的函数
-
装饰器工厂:是一个返回装饰器的函数,需要通过调用(带括号)来使用
-
-
装饰器组合的执行顺序:
-
装饰器工厂:从最上面的装饰器工厂开始,由上到下依次执行
-
装饰器:从最下面的装饰器开始,由下到上依次执行
-
-
例子:
function decorator1(target: Function) {
console.log('decorator1 执行');
}
function factory2() {
console.log('factory2 工厂执行');
return function(target: Function) {
console.log('factory2 返回的装饰器执行');
}
}
function factory3() {
console.log('factory3 工厂执行');
return function(target: Function) {
console.log('factory3 返回的装饰器执行');
}
}
function decorator4(target: Function) {
console.log('decorator4 执行');
}
@decorator1
@factory2()
@factory3()
@decorator4
class DemoClass {}
// 执行结果:
// factory2 工厂执行
// factory3 工厂执行
// decorator4 执行
// factory3 返回的装饰器执行
// factory2 返回的装饰器执行
// decorator1 执行ass TestClass {}
5.5 属性装饰器
定义:属性装饰器是一种特殊类型的声明,它能够被附加到类属性的定义上,对属性的元数据、行为进行修改或者扩展
5.5.1 基本语法
- 语法结构:
function 装饰器名称(target: object, propertyKey: string) {
// 装饰器逻辑
}
class 类名 {
@装饰器名称 属性名: 类型;
}
target
:静态属性–>类本身;实例属性–>类的原型对象propertyKey
:被装饰的属性名
- 例子:
function logProperty(target: object, propertyKey: string) {
console.log(`装饰器被调用: 目标对象 =`, target);
console.log(`装饰器被调用: 装饰的属性名 =`, propertyKey);
}
class Example {
@logProperty name: string;
@logProperty static staticProp: number;
}
// 创建实例
const example = new Example();
//输出结果
//装饰器被调用: 目标对象 = {}
//装饰器被调用: 装饰的属性名 = name
//装饰器被调用: 目标对象 = Example {}
//装饰器被调用: 装饰的属性名 = staticProp
5.5.2 关于属性遮蔽
-
属性遮蔽定义:当实例对象上的属性与原型对象上的属性同名时,实例属性会覆盖(遮蔽)原型属性的现象
-
例子:
class Person {
name: string
age: number
constructor(name: string, age: number) {
this.name = name
this.age = age//仅修改实例自身属性
}
}
const p1 = new Person('张三', 18)
let value = 99
Object.defineProperty(Person.prototype, 'age', {
get() {
return value
},
set(val) {
value = val
}
})
console.log(p1.age) //输出18
console.log(Person.prototype.age) //输出99
代码解释:当通过 new Person('张三', 18)
创建 p1
实例时,构造函数会直接为p1实例添加自身的 name
和 age
数据属性(值分别为 '张三'
和 18
),这一过程是纯粹的实例属性初始化,与原型链无关。随后,通过 Object.defineProperty
在 Person.prototype
上定义了一个名为 age
的访问器属性,其 getter
和 setter
关联到一个全局变量 value
(初始值为 99
)。
当访问 p1.age
时,由于实例自身已存在 age
数据属性,JavaScript 会优先读取实例属性值 18
,而完全绕过原型链上的 age
访问器属性(这种现象称为“属性遮蔽”)。而访问 Person.prototype.age
时,由于原型本身没有存储 age
数据属性,直接触发其访问器属性的 getter
逻辑,返回闭包变量 value
的值 99
class Person {
name: string
age: number
constructor(name: string, age: number) {
this.name = name
this.age = age
}
}
let value = 99
Object.defineProperty(Person.prototype, 'age', {
get() {
return value
},
set(val) {
value = val
}
})
const p1 = new Person('张三', 18)
console.log(p1.age) //输出18
console.log(Person.prototype.age)//输出18
代码解释:由于在创建 p1
实例之前,已经通过 Object.defineProperty
在 Person.prototype
上定义了 age
的访问器属性(getter/setter
),整个构造过程的逻辑发生了根本性改变。当执行 new Person('张三', 18)
时,构造函数中的 this.age = age
并不会直接为实例添加自身属性,而是会触发原型链上已存在的 age
访问器属性的 setter
函数。该 setter
将全局变量 value
从初始的 99
修改为 18
,但此操作不会为实例 p1
创建任何自身的 age
属性。
当访问 p1.age
时,由于实例本身没有 age
数据属性,JavaScript 会沿原型链查找到 Person.prototype.age
的 getter
,此时 getter
返回的是已被修改的全局变量 value
的值 18
。同理,直接访问 Person.prototype.age
也会触发同一 getter
,因此输出结果同样是 18
。这种模式下,所有实例对 age
的读写实际上共享了原型上的 setter/getter
控制的全局变量 value
,导致实例属性与原型访问器属性形成了一种隐式的联动关系。若后续通过 p1.age = 20
修改值,仍会通过原型 setter
更新 value
,而不会为实例添加自身属性
5.5.3 应用举例
function State(target: object, propertyKey: string) {
let value: any;
Object.defineProperty(target, propertyKey, {
get () {
return value
},
set(newVal){
console.log(`${propertyKey}的新值为:${newVal}`);
value = newVal
},
});
}
class Person {
name: string;
@State age: number;
school = 'atguigu';
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
const p1 = new Person('张三', 18);
const p2 = new Person('李四', 30);
p1.age = 80
p2.age = 90
console.log(p1.age)
console.log(p2.age)
执行流程:这段代码定义了一个装饰器函数State
,用于拦截类属性的修改并记录日志。装饰器内部声明了一个闭包变量value
,当它被应用于类Person
的age
属性时,会通过Object.defineProperty
重新定义该属性的getter
和setter
。在类Person
的构造函数中初始化age
时,会触发装饰器的setter
,将传入的年龄值赋给闭包变量value
并打印日志。由于闭包变量value
是装饰器函数内部的变量,且装饰器在类定义阶段只执行一次,因此所有Person
实例的age
属性会共享这同一个value
。当创建p1
实例时,构造函数中this.age = 18
触发setter
,value
被设为18并打印日志;创建p2
实例时,this.age = 30
再次触发setter
,value
被更新为30并打印日志。随后修改p1.age = 80
和p2.age = 90
时,都会触发setter
,value
依次被更新为80和90,每次更新都会打印对应日志。最后访问p1.age
和p2.age
时,调用的getter
会返回同一个闭包变量value
的最终值90,因此两次输出均为90,体现了所有实例共享闭包变量的特性。
function State(target: object, propertyKey: string) {
let key = `__${propertyKey}`;
Object.defineProperty(target, propertyKey, {
get () {
return this[key]
},
set(newVal: string){
console.log(`${propertyKey}的最新值为:${ewVal}`);
this[key] = newVal
},
enumerable: true,
configurable: true,
});
}
class Person {
name: string;
//使⽤State装饰器
@State age: number;
school = 'atguigu';
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
const p1 = new Person('张三', 18);
const p2 = new Person('李四', 30);
p1.age = 80
p2.age = 90
console.log('------------------')
console.log(p1.age) //80
console.log(p2.age) //90
执行流程:这段代码定义了一个装饰器函数State
,用于拦截类属性的修改并记录日志。当装饰器被应用于Person
类的age
属性时,它会在类的原型上创建一个自定义的getter
和setter
,并为每个实例创建一个以__
为前缀的私有属性(如__age
)来存储实际值。在类的构造函数中初始化age
属性时,会触发装饰器的setter
,将传入的初始值存储到实例的私有属性中并打印日志。由于每个实例都有自己独立的私有属性,它们之间不会相互干扰。当创建p1
实例时,构造函数中的this.age = 18
触发setter
,在p1
上创建__age
属性并赋值为18;创建p2
时同理,在p2
上创建独立的__age
属性并赋值为30。后续修改p1.age = 80
和p2.age = 90
时,分别更新各自实例的私有属性并打印日志。最后访问p1.age
和p2.age
时,调用原型上的getter
,分别返回各自实例的私有属性值80和90,因此输出不同的结果。这种实现确保了每个实例的状态独立,避免了共享闭包变量导致的问题。
两段代码的不同点:
-
状态存储方式:第一段代码使用闭包变量
value
存储状态;第二段代码使用实例属性this[key]
(如__age
)存储状态 -
隔离性:第一段代码所有实例共享同一个状态。修改
p2.age
会覆盖p1.age
的值;第二段代码每个实例有独立的状态。修改p1.age
不会影响p2.age
5.6 方法装饰器
定义:在不修改原有方法的前提下,对方法的行为进行扩展或修改。装饰器本质上是一个函数,会在类定义时被调用,接收三个参数来描述被装饰的方法
5.6.1 基本语法
- 语法结构:
function decorator(target: object, propertyKey: string, descriptor: PropertyDescriptor) {
// 可以在这里修改方法行为
return descriptor; // 通常需要返回修改后的描述符
}
class MyClass {
@decorator
myMethod() { /* ... */ }
}
target
:静态方法–>类本身;实例方法–>类的原型对象propertyKey
:被装饰方法的名称descriptor
: 方法的属性描述符对象,包含方法的配置信息
5.6.2 应用举例
function Logger(target: object, propertyKey: string, descriptor: PropertyDescriptor) {
const originnal = descriptor.value
descriptor.value= function (...args:any[]) {
console.log(`${propertyKey}开始执行......`)
const result = originnal .call(this,...args)
console.log(`${propertyKey}执行结束......`)
return result
}
}
class Person {
constructor(
public name:string,
public age:number,
){}
@ Logger speak() {
console.log(`你好,我的名字:${this.name},我的年龄${this.age}`)
}
static isAdult(age:number) {
return age>=18;
}
}
const p1 = new Person('tom',18);
p1.speak()
代码解释:当 TypeScript 编译器处理Person
类时,遇到@Logger
装饰器会立即执行Logger
函数,并传入三个参数:Person
类的原型对象、方法名"speak"
以及该方法的属性描述符。在Logger
函数内部,它保存了原始方法的引用,并将方法的value
属性替换为一个新的包装函数。这个包装函数会在调用原始方法前后添加日志记录,但保持原始方法的参数和返回值不变。
当创建Person
实例并调用speak
方法时,实际上执行的是装饰器替换后的包装函数。该函数首先打印开始日志,然后使用call
方法在正确的实例上下文中调用原始方法,并传入所有参数。这样可以确保原始方法中的this
关键字正确指向实例对象。原始方法执行后,包装函数打印结束日志,并返回原始方法的结果(即使这里没有返回值,也会正确返回undefined
)
function Validate(maxValue:number){
return function (target: object, propertyKey: string, descriptor: PropertyDe
scriptor){
const original = descriptor.value;
descriptor.value = function (...args: any[]) {
if (args[0] > maxValue) {
throw new Error('年龄⾮法!')
}
return original.apply(this, args);// 调用原始的 isAdult 方法
};
}
}
class Person {
constructor(
public name:string,
public age:number,
){}
@Validate(120)
static isAdult(age:number) {
return age >= 18;
}
}
console.log(Person.isAdult(100))
代码解释:通过 Validate
装饰器实现对静态方法 isAdult
的参数验证。首先,Validate(120)
作为装饰器工厂被调用,返回一个装饰器函数,该函数接收三个参数:目标类 Person
、方法名 isAdult
和方法的属性描述符 descriptor
。装饰器内部保存原始方法到 original
变量,然后用新函数替换原方法。新函数会在调用时先验证第一个参数(年龄)是否超过 120,若超过则抛出错误,否则通过 original.apply(this, args)
调用原始方法并返回结果。这里的 apply
确保了两件事:一是保持正确的上下文,使 this
指向类 Person
本身;二是将参数数组 args
正确展开为原始方法所需的参数列表。当执行 Person.isAdult(100)
时,实际调用的是装饰器替换后的新函数,它先验证 100 ≤ 120
通过,然后调用原始的 isAdult
方法,传入参数 100
,最终返回 100 >= 18
的结果 true
。整个过程中,装饰器通过闭包捕获了 maxValue
(120),使得验证逻辑可以在方法调用时动态执行,实现了在不修改原方法的前提下增强其功能的目的
5.7 访问器装饰器
定义:用于修改类的 getter/setter 方法。它本质上是一个函数,可以在不改变原有代码的前提下,对类的访问器进行增强
5.7.1 基本语法
function 装饰器名称(target: object, propertyKey: string, descriptor: PropertyDescriptor) {
// 装饰器逻辑
}
class 类名 {
@装饰器名称
get 属性名() {
// getter 逻辑
}
@装饰器名称
set 属性名(value) {
// setter 逻辑
}
}
target
:静态方法–>类本身;实例方法–>类的原型对象
propertyKey
:被装饰方法的名称
descriptor
: 方法的属性描述符对象,包含方法的配置信息,如:get
: getter 函数;set
: setter 函数;enumerable
: 是否可枚举;configurable
: 是否可配置
5.7.2 应用举例
function RangeValidate(min: number, max: number) {
return function (target: object, propertyKey: string, descriptor: PropertyDe
scriptor) {
const originalSetter = descriptor.set;
descriptor.set = function (value: number) {
if (value < min || value > max) {
throw new Error(`${propertyKey}的值应该在 ${min} 到 ${max}之间!`);
}
if (originalSetter) {
originalSetter.call(this, value);
}
};
};
}
class Weather {
private _temp: number;
constructor(_temp: number) {
this._temp = _temp;
}
@RangeValidate(-50,50)
set temp(value) {
this._temp = value;
}
get temp() {
return this._temp;
}
}
const w1 = new Weather(25);
console.log(w1)
w1.temp = 67
console.log(w1)
代码解释:当编译器处理`Weather`类时,遇到`@RangeValidate(-50, 50)`装饰器会立即执行`RangeValidate`工厂函数,传入参数`min=-50`和`max=50`。工厂函数返回一个实际的装饰器函数,该函数接收三个参数:类的原型对象、属性名`"temp"`和属性描述符。在装饰器内部,它保存了原始的`temp`属性 setter 方法,并将其替换为一个新的包装函数。这个包装函数会在每次赋值时检查值是否在`[-50, 50]`范围内,如果超出范围则抛出错误,否则调用原始 setter 方法设置值。
当创建`Weather`实例时,构造函数直接设置`_temp`属性(绕过了装饰器验证,因为构造函数直接访问私有属性)。但当通过`w1.temp = 67`赋值时,实际执行的是装饰器重写后的 setter 方法。该方法首先验证`67`超出范围,抛出错误`"temp的值应该在 -50 到 50之间!"`,导致赋值失败。
## 5.8 参数装饰器
定义:参数装饰器是一种特殊的声明,用于为类的方法参数添加元数据或逻辑。它本质上是一个函数,会在类定义时被调用,对参数进行处理
作用:可以在不修改方法核心逻辑的情况下,对参数进行验证、转换或记录信息,提高代码的可维护性和复用性
### 5.8.1 基本语法
function 参数装饰器名(target: object, propertyKey: string, parameterIndex: number) {
// 装饰器逻辑
}
> `target`:如果修饰的是【实例方法】的参数,target 是类的【原型对象】;如果修饰的是【静态方法】的参数,target 是【类】
>
> `propertyKey`:参数所在的方法的名称
>
> `parameterIndex`:参数在函数参数列表中的索引,从 0 开始
function NotNumber(target: any, propertyKey: string, parameterIndex: number) {
let notNumberArr: number[] = target[`__notNumber_${propertyKey}`] || [];
notNumberArr.push(parameterIndex);
target[`__notNumber_${propertyKey}`] = notNumberArr;
}
function Validate(target: any, propertyKey: string, descriptor: PropertyDescri
ptor) {
const method = descriptor.value;
descriptor.value = function (...args: any[]) {
const notNumberArr: number[] = target[`__notNumber_${propertyKey}`] || [];
for (const index of notNumberArr) {
if (typeof args[index] === 'number') {
throw new Error(`⽅法 ${propertyKey} 中索引为 ${index} 的参数不能是数字!`)
}
}
return method.apply(this, args);
};
return descriptor;
}
class Student {
name: string;
constructor(name: string) {
this.name = name;
}
@Validate
speak(@NotNumber message1: any, mesage2: any) {
console.log(`${this.name}想对说:${message1},${mesage2}`);
}
}
const s1 = new Student("张三");
s1.speak(100, 200);
代码解释:首先来看参数装饰器 NotNumber
。当它被应用到某个方法的参数上时,会在类的原型对象上创建一个数组,用于记录哪些参数不允许是数字类型。具体来说,它接收三个参数:类的原型对象 target
、方法名 propertyKey
和参数在参数列表中的索引 parameterIndex
。它会从 target
对象中获取或初始化一个名为 __notNumber_${propertyKey}
的数组,然后将当前参数的索引添加到这个数组中。这样,我们就通过装饰器在类的原型对象上标记了哪些参数不允许为数字类型。
接下来是方法装饰器 Validate
,它负责实际的参数验证工作。这个装饰器会修改被装饰方法的行为,在方法执行前进行参数检查。它接收类的原型对象 target
、方法名 propertyKey
和方法描述符 descriptor
作为参数。装饰器内部会保存原始方法的引用,然后用一个新的函数替换原方法。这个新函数在执行时,会先从 target
对象中获取之前由 NotNumber
装饰器存储的参数索引数组,然后遍历这个数组,检查对应位置的参数是否为数字类型。如果发现某个参数是数字类型,就会抛出一个错误;如果所有参数都通过验证,才会调用原始方法并返回结果。
在 Student
类中,这两个装饰器被组合使用。speak
方法被 @Validate
装饰器修饰,而它的第一个参数 message1
被 @NotNumber
装饰器修饰。这意味着当 speak
方法被调用时,会先执行 Validate
装饰器添加的验证逻辑,检查索引为 0 的参数(也就是 message1
)是否为数字类型。
整个过程的执行顺序是:当解释器处理类定义时,会先执行 @NotNumber
装饰器,将参数索引 0 存入类的原型对象中;接着执行 @Validate
装饰器,修改 speak
方法的实现。当实际调用 speak
方法时,就会执行被修改后的方法,该方法会读取之前存储的索引信息并进行参数验证。
当我们创建 Student
类的实例并调用 speak
方法时,如果第一个参数传入了数字类型的值,就会触发验证逻辑并抛出错误,提示该参数不能是数字类型。这样,通过装饰器的组合使用,我们实现了参数验证逻辑与业务逻辑的分离,提高了代码的可维护性和复用性