ts装饰器使用(上)
定义
装饰器是一种新的声明方式,它可以被用来修饰类、方法、访问器、属性或参数。装饰器的语法是在被修饰的声明前面加上 @ 符号,然后后面跟着一个标识符,例如 @decorator
。这个 decorator 必须是一个函数,或者是一个求值后返回函数的表达式。
当程序运行时,装饰器会被调用,被修饰的声明会作为参数传递给装饰器函数。装饰器的应用位置必须紧跟在要修饰的内容之前。
需要注意的是,装饰器不能被用在声明文件(.d.ts)中,也不能用在任何外部上下文中,比如 declare 关键字。
装饰器函数实际上是一个普通的函数,但是它具有特殊的行为。装饰器函数接收一个参数,即要装饰的目标(通常是一个类的构造函数)。装饰器的作用可以根据具体需求来定义,它可以在不修改类本身的情况下,为类添加额外的功能或者元数据。
当你使用装饰器时,使用 @装饰器函数名 的语法,将装饰器函数放在要装饰的内容(比如类、方法、属性等)的前面。在装饰器函数内部,你可以通过参数 target 访问到被装饰的目标,并且进行一些操作,比如给目标添加属性、修改方法行为等。
装饰器的强大之处在于它能够让你在不改动原始代码的情况下,动态地扩展或修改类的行为。 这种灵活性使得装饰器成为了 TypeScript 和 JavaScript 中一个非常有用的特性。
装饰器工厂
在 TypeScript 中,装饰器工厂是一个函数,该函数返回一个装饰器函数。装饰器工厂用来创建动态的装饰器,它允许你在运行时根据不同的条件生成不同的装饰器逻辑。装饰器工厂的基本结构如下:
function decoratorFactory(...args: any[]): ClassDecorator | MethodDecorator | PropertyDecorator | ParameterDecorator {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
// 装饰器逻辑
};
}
@decoratorFactory(arg1, arg2, ...)
class MyClass {
// 类的定义
}
decoratorFactory
是一个装饰器工厂函数,它接受任意数量的参数,并且返回一个装饰器函数。这个装饰器函数可以应用于类、类的属性、类的方法、以及方法的参数上。
具体地说,这个装饰器工厂函数的返回类型是 ClassDecorator | MethodDecorator | PropertyDecorator | ParameterDecorator
。这是因为装饰器可以应用于多种不同的地方,所以我们在装饰器工厂函数的返回类型中列出了所有可能的类型。
装饰器工厂函数返回的装饰器函数接受三个参数:target(对于方法装饰器是类的原型对象,对于属性装饰器是类的构造函数,对于参数装饰器是类的原型对象),propertyKey(属性或方法的名字,对于方法装饰器是方法的名字,对于参数装饰器是参数的名字),以及 descriptor(属性描述符,对于方法装饰器是方法的描述符)。
在这个装饰器函数内部,你可以编写具体的装饰器逻辑。这个逻辑可以根据传入的参数进行不同的处理,也可以根据不同的装饰位置(类、属性、方法、参数)进行不同的操作。
最后,@decoratorFactory(arg1, arg2, ...)
这样的语法表示将装饰器工厂函数返回的装饰器应用于 MyClass 类。在应用装饰器时,你可以传递任意数量的参数给装饰器工厂函数,这些参数可以用来定制装饰器的行为。
简单理解来说
function setProp () {
return function (target) {
// ...
}
}
@setProp()
setProp
是一个装饰器工厂函数,它返回一个装饰器函数。@setProp() 使用了装饰器工厂的返回值作为装饰器。装饰器工厂的作用是在运行时根据传入的参数动态生成装饰器逻辑。在装饰器工厂内部,你可以根据需要定义不同的装饰器行为,然后返回这个装饰器函数。
这种方法使得装饰器的行为可以在运行时动态确定,提供了更大的灵活性。例如,你可以根据环境变量、用户权限等条件来决定使用哪种装饰器逻辑。
装饰器组合使用
在TypeScript中,装饰器可以组合,也就是可以同时应用多个装饰器在同一个目标上
@setDec1
@setDec2
target
需要注意这些装饰器的执行顺序。首先,装饰器工厂从上到下依次执行,但是它们只是用于返回函数,不会立即执行。然后,装饰器函数从下到上依次执行,也就是执行装饰器工厂返回的函数。这种顺序确保了装饰器的嵌套关系按照从外到内的顺序执行。
首先,装饰器工厂(decorator factories)按照代码中的顺序从上到下依次执行。这些工厂函数返回的实际装饰器函数并不会立即执行,而只是被存储起来。
在被装饰的目标(比如类、方法、属性等)被实例化或者调用时,装饰器函数按照从下到上的顺序依次执行。
先执行在代码中写在最底部的装饰器函数,然后是倒数第二个,以此类推,最后执行在代码中写在最顶部的装饰器函数。这种顺序确保了装饰器的嵌套关系按照从外到内的顺序执行,即从最外层的装饰器到最内层的装饰器。
这个执行顺序非常重要,因为它确保了装饰器的应用顺序与代码中的顺序一致,而不会混乱。
下面例子中setDec1
和 setDec2
都是装饰器工厂函数。当 @setDec1() 和 @setDec2() 应用到 Test 类上时,它们被调用并返回实际的装饰器函数。
setDec1 装饰器工厂被调用,输出 'get setDec1',然后它返回一个装饰器函数。这个装饰器函数被存储下来,但并不立即执行。
setDec2 装饰器工厂被调用,输出 'get setDec2',然后它返回另一个装饰器函数。同样,这个装饰器函数被存储下来,但并不立即执行。
当装饰器应用到 Test 类上时,存储的装饰器函数被执行。因为 setDec2 装饰器在 setDec1 装饰器的下面,所以 setDec2 的装饰器函数先被执行,输出 'setDec2'。然后,setDec1 的装饰器函数被执行,输出 'setDec1'。
function setDec1 () {
console.log('get setDec1')
return function (target) {
console.log('setDec1')
}
}
function setDec2 () {
console.log('get setDec2')
return function (target) {
console.log('setDec2')
}
}
@setDec1()
@setDec2()
class Test {}
// 打印出来的内容如下:
/**
'get setDec1'
'get setDec2'
'setDec2'
'setDec1'
*/
装饰器求值
类装饰器
let sign = null;
function setName(name: string) {
return function(target: Function) {
sign = target;
console.log(target.name);
};
}
@setName("lison") // Info
class Info {
constructor() {}
}
console.log(sign === Info); // true
console.log(sign === Info.prototype.constructor); // true
setName 函数:
setName 是一个函数,它接收一个字符串参数 name
,返回一个装饰器函数。
这个装饰器函数接收一个参数 target
,它是被装饰的类的构造函数。
在函数内部,它将传入的类构造函数(target)赋值给外部声明的 sign 变量,并输出类的名称。
@setName("lison")
是装饰器的语法,它将 "lison" 作为参数传递给 setName 函数。
当装饰器被应用到 Info 类时,setName 函数被调用,target 参数被设置为 Info 类的构造函数。
target.name 表示类的名称,即 "Info" ,它被输出到控制台。
类的判断:
console.log(sign === Info)
; 输出 true,因为 sign 变量被赋值为 Info 类的构造函数。
console.log(sign === Info.prototype.constructor);
也输出 true,因为 Info.prototype.constructor 指向 Info 类的构造函数。
代码演示了一个类装饰器如何在类声明前修改类的构造函数,并且通过 setName 函数内部的逻辑,将类的构造函数(target)保存到外部的 sign 变量中,从而实现了对类的额外处理。
方法装饰器
方法装饰器是一种特殊类型的装饰器,用于处理类中的方法。它提供了对方法的属性描述符和方法定义进行操作的机会。在运行时,方法装饰器被当做函数调用,并且它接受三个参数:
装饰静态成员时:
第一个参数是类的构造函数。当方法装饰器被应用于类的静态方法时,该参数表示类本身,即类的构造函数。
装饰实例成员时:
第一个参数是类的原型对象。当方法装饰器被应用于类的实例方法时,该参数表示类的原型对象,即类的实例的原型。
第二个参数:
表示成员的名字。对于类的实例方法,这就是方法的名称。
第三个参数:
表示成员的属性描述符。这是一个 JavaScript 对象,描述了该方法的各种属性,比如是否可写、是否可配置等。
通过这三个参数,方法装饰器可以对方法的行为和特性进行修改和增强,例如修改方法的实现、拦截方法调用、记录方法的调用日志等。方法装饰器的运行顺序与其他装饰器类似,是从上到下的。
总之,方法装饰器为开发者提供了一种强大的机制,用于在类的方法上执行自定义的逻辑,从而实现更灵活、可维护的代码。
在JavaScript中,属性描述符(property descriptor)是指一个对象的属性具有的特性,包括可配置性(configurable)、可写性(writable)和可枚举性(enumerable)。这些特性决定了属性的行为和可访问性。
可配置性(configurable):
如果属性的 configurable 特性为 true,那么该属性的描述符可以被修改,也可以被删除。如果为 false,则属性的描述符不能被修改,也不能被删除。
可写性(writable):
如果属性的 writable 特性为 true,那么该属性的值可以被修改。如果为 false,则该属性的值不能被修改。
可枚举性(enumerable):
如果属性的 enumerable 特性为 true,那么该属性可以被枚举(通过 for...in 循环或 Object.keys() 方法遍历)。如果为 false,则该属性不能被枚举。
对于属性描述符,在ES5中,引入了Object.defineProperty()
方法,该方法允许开发者在创建或修改对象的属性时,明确定义属性的特性,包括上述的可配置性、可写性和可枚举性。这为开发者提供了更精细的控制对象属性行为的能力。
以下是一个使用Object.defineProperty()方法定义属性描述符的示例:
var obj = {};
Object.defineProperty(obj, 'property1', {
value: 42,
writable: false, // 不可修改
enumerable: true, // 可枚举
configurable: true // 可配置
});
console.log(obj.property1); // 输出 42
obj.property1 = 100; // 这里不会修改成功,因为 writable 设置为 false
console.log(obj.property1); // 仍然输出 42
function enumerable(bool: boolean) {
return function(
target: any,
propertyName: string,
descriptor: PropertyDescriptor
) {
console.log(target); // { getAge: f, constructor: f }
descriptor.enumerable = bool;
};
}
class Info {
constructor(public age: number) {}
@enumerable(false)
getAge() {
return this.age;
}
}
const info = new Info(18);
console.log(info);
// { age: 18 }
for (let propertyName in info) {
console.log(propertyName);
}
// "age"
上述案例演示了如何使用方法装饰器控制类方法的可枚举性(enumerable)。在这个例子中,方法装饰器@enumerable(false)
被应用于Info类中的getAge方法。装饰器内部定义了一个函数,该函数接受三个参数:target(类的原型对象),propertyName(方法的名称),descriptor(方法的属性描述符)。
在这个装饰器中,descriptor.enumerable属性被设置为传入的bool参数的值,决定了getAge方法是否可枚举。如果bool为false,则getAge方法将不会出现在for...in循环中。
代码中的输出结果解释如下:
console.log(target)
; 输出了target,即{ getAge: f, constructor: f },表示这个装饰器应用于Info类的原型对象,修改了原型对象上的getAge方法的属性描述符。
console.log(info); 输出了{ age: 18 },表示Info类的实例对象info,其中age属性是可见的。
for (let propertyName in info) { console.log(propertyName); }
遍历了info对象的属性。由于getAge方法的enumerable属性被设置为false,所以它不会出现在for...in循环中,只输出了age属性。
通过装饰器,我们可以灵活地控制类的成员的行为,包括可见性、可枚举性等。在这个例子中,装饰器限制了getAge方法的可枚举性,使其在对象遍历中不可见。
转载自:https://juejin.cn/post/7299027624172486682