装饰器就是一个函数,通过装饰器能够扩展代码的功能但是不修改原代码。nestjs的很大一部分功能都是由装饰器实现的。
装饰器的缺点也很明显,它会让代码实现的细节被隐藏,让代码逻辑变的黑盒,会让结果变得难以理解。但是装饰器填补了javascript元编程能力的空白。虽然装饰器的提供的功能也可以通过辅助函数来实现,但是会让代码的设计和结构乱糟糟的,非常不合理。
看一下装饰器和辅助函数这两种方式实现添加日志记录功能的区别:
- 使用装饰器给类成员函数添加日志记录的功能
function log(target:Object,propertyKey:string,descriptor:PropertyDescriptor){
//获取老的函数
const originalMethod = descriptor.value;
//重定原型上的属性
descriptor.value = function (...args: any[]) {
console.log(`调用方法${propertyKey},记录下日志`, 'dosomething....');
const result = originalMethod.apply(this,args);
return result;
}
}
class User{
@log
changeUserPwd() {
// 修改密码
return true
}
@log
changeUserName() {
// 修改姓名
return true
}
}
const user = new User();
user.changeUserPwd();
user.changeUserName();
- 辅助函数的方式:
function log(fn: Function){
//获取老的函数
console.log(`调用方法${fn.name},记录下日志`, 'dosomething....');
const result = fn();
return result;
}
class User{
changeUserPwd() {
// 修改密码
return true
}
changeUserName() {
// 修改姓名
return true
}
}
const user = new User();
log(user.changeUserPwd)
log(user.changeUserName);
辅助函数的方式让log函数与类方法进行分离。不是声明式的。想要给记录日志,还要调用下log方法。
如果是装饰器的方式就可以直接调用user的changeUserPwd
方法,方法内部会自动调用装饰器,这样就一步到位了,在定义的时候就决定了这个方法被调用时就要记录下日志。
装饰器最大作用就是可用于元编程,并为装饰的类型添加或改变功能,而不用通过外部行为来添加/修改功能。
装饰器是js的还是ts的,傻傻分不清
装饰器是js语法,但是目前还是提案阶段,装饰器提案已经五年了, 目前已经到了第三阶段, 因为是提案阶段,所以在浏览器环境或者node环境下是不识别装饰器语法的。
所以如果我们想要使用装饰器则可以用babel或者TypeScript 的编译器tsc来编译装饰器语法,这样就可以在浏览器或者node环境中使用了。
装饰器的类型
我们使用typescript编译器来编译装饰器语法(在tsconfig文件中启用experimentalDecorators
)。
- 类装饰器(Class Decorators) :应用于类构造函数,可以用于修改类的定义。
- 方法装饰器(Method Decorators) :应用于方法,可以用于修改方法的行为。
- 访问器装饰器(Accessor Decorators) :应用于类的访问器属性(getter 或 setter)。
- 属性装饰器(Property Decorators) :应用于类的属性。
- 参数装饰器(Parameter Decorators) :应用于方法参数。
类装饰器
-
简单类装饰器
参数是类本身
function logClass(constructor: Function) {
console.log("Class created:", constructor.name);
}
@logClass
class Person {
constructor(public name: string) {}
}
// 输出: Class created: Person
-
类装饰器工厂
返回装饰器函数
function logClassWithParams(message: string) {
return function (constructor: Function) {
console.log(message, constructor.name);
};
}
@logClassWithParams("Creating class:")
class Car {
constructor(public model: string) {}
}
// 输出: Creating class: Car
-
修改类的行为
扩展/修改类的属性
function addTimestamp<T extends { new(...args: any[])>(constructor: T) {
return class extends constructor {
timestamp = new Date();
};
}
// 合并声明
interface Document{
timestamp: Date;
}
@addTimestamp
class Document {
constructor(public title: string) {}
}
const doc = new Document("My Document");
//const doc = new Document("My Document") as Document & { timestamp: Date };
console.log(doc.title); // My Document
console.log(doc.timestamp); // 当前日期和时间
export {}
扩展/修改类的构造函数
function replaceConstructor<T extends { new(...args: any[]): {} }>(constructor: T) {
return class extends constructor {
constructor(...args: any[]) {
super(...args);
console.log("Instance created");
}
};
}
@replaceConstructor
class User {
constructor(public name: string) {}
}
const user = new User("Alice");
// 输出: Instance created
- 类装饰器还可以重写类方法
function aa(target: new (...args: any) => any) {
return class extends target {
eat() {
super.eat() // 调用target类的eat方法,不写这行也行
console.log('子类 eat方法')
}
}
}
@aa
class Animal {
eat() {
console.log('父类eat方法')
}
}
const animal = new Animal()
console.log(animal)
animal.eat()
animal.eat()
调用的是重写后的方法。
缺点:重写后的原来类本身的方法无法用类实例访问了。
方法装饰器
函数是javascript的一等公民,方法装饰器的功能非常强大,方法装饰器可以修改原方法的行为、添加元数据、日志记录、权限检查等。
方法装饰器是一个接受三个参数的函数:
target
:装饰的目标对象,对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。propertyKey
:装饰的成员名称。descriptor
:成员的属性描述符。
- 日志记录
function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Calling ${propertyKey} with arguments: ${args}`);
const result = originalMethod.apply(this, args);
console.log(`Result: ${result}`);
return result;
};
return descriptor;
}
class Calculator {
@log
add(a: number, b: number): number {
return a + b;
}
}
const calc = new Calculator();
calc.add(2, 3);
-
权限检查
//可以在方法调用前检查 用户的权限,决定是否可以调用
let users = {
'001':{roles:['admin']},
'002':{roles:['member']}
}
function authorize(target:any,propertyKey:string,descriptor:PropertyDescriptor){
//获取老的函数
const originalMethod = descriptor.value;
//重定原型上的属性
descriptor.value = function(roleId: keyof typeof users){
let user = users[roleId]
if(user&&user.roles.includes('admin')){
originalMethod.apply(this,[roleId])
}else{
throw new Error(`User is not authorized to call this method`)
}
}
return descriptor;
}
class AdminPanel{
@authorize
deleteUser(userId:string){
console.log(`User ${userId} is deleted`)
}
}
const adminPanel = new AdminPanel();
adminPanel.deleteUser('002');
-
方法缓存
缓存方法的返回结果,以提高性能。
function cache(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
const cacheMap = new Map<string, any>();
descriptor.value = function (...args: any[]) {
const key = JSON.stringify(args);
if (cacheMap.has(key)) {
return cacheMap.get(key);
}
console.log(`计算${key}`)
const result = originalMethod.apply(this, args);
cacheMap.set(key, result);
return result;
};
return descriptor;
}
class MathOperations {
@cache
factorial(n: number): number {
if (n <= 1) {
return 1;
}
return n * this.factorial(n - 1);
}
}
const mathOps = new MathOperations();
console.log(mathOps.factorial(5)); // 120
console.log(mathOps.factorial(5)); // 从缓存中获取结果
- 让简写形式的类方法可枚举
function Enum(isEnum: boolean) {
return function (target, propertyKey, descriptor) {
console.log('target: ', target)
console.log('propertyKey: ', propertyKey)
console.log('descriptor: ', descriptor)
descriptor.enumerable = isEnum
}
}
class Animal {
@Enum(true)
eat() {
console.log('anmial eat 方法')
}
}
const animal = new Animal()
console.log(animal)
访问符装饰器
装饰类的访问器属性(getter 和 setter)。访问器装饰器可以用于修改或替换访问器的行为,添加元数据,进行日志记录等。
访问器装饰器是一个接受三个参数的函数:
target
:装饰的目标对象,对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。propertyKey
:访问器的名称。descriptor
:访问器的属性描述符。
- 日志记录
可以在访问器的 get
和 set
方法中添加日志记录,以跟踪属性的访问和修改
function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalGet = descriptor.get;
const originalSet = descriptor.set;
if (originalGet) {
descriptor.get = function() {
const result = originalGet.apply(this);
console.log(`Getting value of ${propertyKey}: ${result}`);
return result;
};
}
if (originalSet) {
descriptor.set = function(value: any) {
console.log(`Setting value of ${propertyKey} to: ${value}`);
originalSet.apply(this, [value]);
};
}
return descriptor;
}
class User {
private _name: string;
constructor(name: string) {
this._name = name;
}
@log
get name() {
return this._name;
}
set name(value: string) {
this._name = value;
}
}
const user = new User("Alice");
console.log(user.name); // Getting value of name: Alice
user.name = "Bob"; // Setting value of name to: Bob
console.log(user.name); // Getting value of name: Bob
-
权限控制
在访问器中添加权限检查,以控制属性的访问权限。
function adminOnly(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalGet = descriptor.get;
descriptor.get = function() {
const user = { role: 'user' }; // 示例用户对象
if (user.role !== 'admin') {
throw new Error("Access denied");
}
return originalGet.apply(this);
};
return descriptor;
}
class SecureData {
private _secret: string = "top secret";
@adminOnly
get secret() {
return this._secret;
}
}
const data = new SecureData();
try {
console.log(data.secret); // 抛出错误: Access denied
} catch (error) {
console.log(error.message);
}
访问符装饰器在nestjs中用到的极少。
属性装饰器
修饰类的属性。属性装饰器用于添加元数据或进行属性初始化等操作,但不同于方法装饰器和类装饰器,它不能直接修改属性的值或属性描述符。
属性装饰器是一个接受两个参数的函数:
target
:装饰的目标对象,对于静态属性来说是类的构造函数,对于实例属性是类的原型对象。propertyKey
:装饰的属性名称。
-
元数据添加
属性装饰器常用于添加元数据,可以结合 Reflect
API 使用,以便在运行时获取元数据。
import "reflect-metadata";
function required(target: any, propertyKey: string) {
Reflect.defineMetadata("required", true, target, propertyKey);
}
class User {
@required
username: string;
}
function validate(user: User) {
for (let key in user) {
if (Reflect.getMetadata("required", user, key) && !user[key]) {
throw new Error(`Property ${key} is required`);
}
}
}
const user = new User();
user.username = "";
validate(user); // 抛出错误:Property username is required
-
属性访问控制
使用属性装饰器来定义属性的访问控制或初始值设置。
function defaultValue(value: string) {
return function (target: any, propKey: string) {
let val = value;
const getter = function () {
return val;
};
const setter = function (newVal: string) {
val = newVal;
};
Object.defineProperty(target, propKey, {
enumerable: true,
configurable: true,
get: getter,
set: setter,
});
};
}
class Settings {
@defaultValue("dark")
theme!: string;
}
const s1 = new Settings();
console.log(s1.theme, "--theme");//dark --theme
- 对属性进行修改
- 属性装饰器不能直接修改属性值或描述符,只能用于添加元数据或做一些初始化操作。
- 属性装饰器通常与其他类型的装饰器(如方法装饰器、类装饰器)配合使用,以实现更复杂的功能。
参数装饰器
修饰类构造函数或方法的参数。参数装饰器主要用于为参数添加元数据,以便在运行时能够获取这些元数据并进行相应的处理。与其他装饰器不同,参数装饰器不直接修改参数的行为或值。
参数装饰器是一个接受三个参数的函数:
target
:装饰的目标对象,对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。propertyKey
:参数所属的方法的名称。parameterIndex
:参数在参数列表中的索引。
-
参数验证
使用参数装饰器在方法调用时验证参数的值。
// 引入 reflect-metadata 库,用于反射元数据操作
import "reflect-metadata";
// 参数装饰器函数,用于验证方法参数
function validate(target: any, propertyKey: string, parameterIndex: number) {
// 获取现有的必需参数索引数组,如果不存在则初始化为空数组
const existingRequiredParameters: number[] = Reflect.getOwnMetadata("requiredParameters", target, propertyKey) || [];
// 将当前参数的索引添加到必需参数索引数组中
existingRequiredParameters.push(parameterIndex);
// 将更新后的必需参数索引数组存储到方法的元数据中
Reflect.defineMetadata("requiredParameters", existingRequiredParameters, target, propertyKey);
}
// 方法装饰器函数,用于在方法调用时验证必需参数
function validateParameters(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
// 保存原始方法
const method = descriptor.value;
// 修改方法,使其在调用时验证必需参数
descriptor.value = function (...args: any[]) {
// 获取方法的必需参数索引数组
const requiredParameters: number[] = Reflect.getOwnMetadata("requiredParameters", target, propertyKey) || [];
// 遍历必需参数索引数组,检查相应的参数是否为 undefined
for (let parameterIndex of requiredParameters) {
if (args[parameterIndex] === undefined) {
// 如果必需参数为 undefined,则抛出错误
throw new Error(`Missing required argument at position ${parameterIndex}`);
}
}
// 调用原始方法并返回其结果
return method.apply(this, args);
};
}
// 定义 User 类
class User {
// 构造函数,初始化 name 属性
constructor(private name: string) {}
// 使用 validateParameters 方法装饰器装饰 setName 方法
@validateParameters
setName(@validate newName: string) {
// 设置新的 name 属性值
this.name = newName;
}
}
// 创建一个 User 实例
const user = new User("Alice");
// 调用 setName 方法,传入有效参数
user.setName("Bob"); // 正常
// 调用 setName 方法,传入 undefined 作为参数,触发参数验证错误
user.setName(undefined); // 抛出错误: Missing required argument at position 0
// 导出一个空对象,以避免模块级别作用域污染
export {}
- 参数装饰器只能应用于方法的参数,不能应用于类或属性。
- 参数装饰器通常依赖
Reflect
API 来存储和访问元数据,因此需要引入reflect-metadata
库,并在tsconfig.json
中启用emitDecoratorMetadata
选项。
装饰器的执行顺序
执行顺序
-
属性装饰器(Property Decorators) 和方法装饰器(Method Decorators) 以及访问器装饰器(Accessor Decorators)
- 按照它们在类中出现的顺序,从上到下依次执行。
-
参数装饰器(Parameter Decorators)
- 在执行方法装饰器之前执行,按照参数的位置从右到左依次执行。
- 对于同一个参数的多个装饰器,也是从从右向左依次执行
-
类装饰器(Class Decorators)
- 最后执行。
// 不同类型的装饰器的执行顺序
/**
* 1.属性装饰器、方法装饰器、访问器装饰器它们是按照在类中出现的顺序,从上往下依次执行
* 2.类装饰器最后执行
* 3.参数装饰器先于方法执行
*/
function classDecorator1(target){
console.log('classDecorator1')
}
function classDecorator2(target){
console.log('classDecorator2')
}
function propertyDecorator1(target,propertyKey){
console.log('propertyDecorator1')
}
function propertyDecorator2(target,propertyKey){
console.log('propertyDecorator2')
}
function methodDecorator1(target,propertyKey){
console.log('methodDecorator1')
}
function methodDecorator2(target,propertyKey){
console.log('methodDecorator2')
}
function accessorDecorator1(target,propertyKey){
console.log('accessorDecorator1')
}
function accessorDecorator2(target,propertyKey){
console.log('accessorDecorator2')
}
function parametorDecorator4(target,propertyKey,parametorIndex:number){
console.log('parametorDecorator4',propertyKey)//propertyKey方法名
}
function parametorDecorator3(target,propertyKey,parametorIndex:number){
console.log('parametorDecorator3',propertyKey)//propertyKey方法名
}
function parametorDecorator2(target,propertyKey,parametorIndex:number){
console.log('parametorDecorator2',propertyKey)//propertyKey方法名
}
function parametorDecorator1(target,propertyKey,parametorIndex:number){
console.log('parametorDecorator1',propertyKey)//propertyKey方法名
}
@classDecorator1
@classDecorator2
class Example{
@accessorDecorator1
@accessorDecorator2
get myProp(){
return this.prop;
}
@propertyDecorator1
@propertyDecorator2
prop!:string
@methodDecorator1
@methodDecorator2
method(@parametorDecorator4 @parametorDecorator3 param1:any,@parametorDecorator2 @parametorDecorator1 param2:any){}
}
//如果一个方法有多个参数,参数装饰器会从右向左执行
//一个参数也可有会有多个参数装饰 器,这些装饰 器也是从右向左执行的
总结
装饰器本质上是一个函数,它可以用来修改类、方法、属性、访问器或者参数。添加装饰器就像是给它们穿上了一层魔法铠甲。每次调用方法的时候都要先经过这个装饰器魔法铠甲,经过铠甲处理后然后才是我们代码逻辑本身进行处理。装饰器也不都是优点,就像上文中说的,装饰器的执行顺序、装饰器代码黑盒,影响我们判断返回结果,调试这些问题,但是装饰器就是nestjs的基础,nestjs中大量使用了装饰器功能,还利用装饰器实现了很多很酷的设计模式、如AOP切面编程,把横切的一些关注点(比如日志、权限验证)从业务逻辑中进行分离,让代码更加清晰。