本文深入解析 TypeScript 中的装饰器特性,涵盖类、方法、访问器、属性及参数五种装饰器的定义与用法。详细阐述装饰器工厂的创建方式,对比普通装饰器与工厂函数的执行顺序差异,并说明在 tsconfig.json 中启用 experimentalDecorators 的配置方法。
前言:
装饰器Decorator 已在ECMAScript标准中提出提案,但目前尚未最终定案。在TypeScript中,该特性已被实现,不过仍属于实验性功能。若需使用装饰器,必须在tsconfig.json配置文件中将experimentalDecorators属性设置为true。
1.概念
1.1定义
装饰器是一种新的声明形式,可应用于类声明、方法、访问器、属性以及参数。使用装饰器时,需采用@符号后接函数名称,例如@testDecorator。其中,testDecorator必须是一个函数,或者返回一个函数。该函数在运行时被调用,被装饰的声明将作为参数自动传入。
需要注意的是,装饰器必须紧邻所修饰内容的前面书写。此外,装饰器不能用于声明文件(.d.ts)以及任何外部上下文中(例如declare)。
装饰器的定义及使用方式如下所示:
// 定义一个函数作为装饰器函数使用
function testDecorator() {}
// 通过@符号使用装饰器
@testDecorator
1.2装饰器工厂
装饰器工厂同样是一个函数,与普通装饰器函数的区别在于,它返回的是一个函数,该返回的函数作为实际调用时的装饰器。使用装饰器工厂时,可根据当前场景传递不同参数,但使用时需加上函数调用括号。
示例代码如下:
// 装饰器工厂,返回值是一个函数
function testDecorator() {
return function() {}
}
// 通过@符号 + 函数调用的方式使用装饰器
@testDecorator()
1.3装饰器组合使用
装饰器支持组合使用,即可以对同一目标应用多个装饰器。
示例代码如下所示:
// 定义两个装饰器函数
function setName() {}
function setAge() {}
// 使用装饰器
@setName
@setAge
class Person {}
当使用多个装饰器时,其执行具有特定顺序,具体如下:
若使用普通装饰器函数,执行顺序是从下往上依次执行。
示例代码如下:
function setName(constructor: any) {
console.log('setName', constructor)
}
function setAge(constructor: any) {
console.log('setAge', constructor)
}
@setName
@setAge
class Person {}
/* 执行结果如下:
setAge [Function: Person]
setName [Function: Person]
*/
若使用装饰器工厂,其执行顺序为先从上到下依次执行工厂函数,随后从下往上依次执行工厂函数返回的函数。示例代码如下:
function setName() {
console.log('get setName')
return function (constructor: any) {
console.log('setName', constructor)
}
}
function setAge() {
console.log('get setAge')
return function (constructor: any) {
console.log('setAge', constructor)
}
}
@setName()
@setAge()
class Person {}
/* 执行结果如下:
get setName
get setAge
setAge [Function: Person]
setName [Function: Person]
*/
1.4装饰器求值
在类定义中,不同声明上的装饰器将按照以下规定顺序被引用:
- 参数装饰器、方法装饰器、访问符装饰器或属性装饰器应用于每个实例成员;
- 参数装饰器、方法装饰器、访问符装饰器或属性装饰器应用于每个静态成员;
- 参数装饰器应用于构造函数;
- 类装饰器应用于类。
2.类装饰器
类装饰器在类声明之前使用,必须紧邻需要装饰的内容,类装饰器直接应用于类的声明。
类装饰器表达式在运行时作为函数被调用,它接收一个参数,即该类的构造函数。
示例代码如下:
let sign = null
function setName() {
return function (constructor: Function) {
sign = constructor
}
}
@setName()
class Info {
constructor() {}
}
console.log(sign === Info) // true
console.log(sign === Info.prototype.constructor) // true
由上述代码可知,类Info的原型对象的constructor属性指向的即为Info本身。
我们还可以通过装饰器修改类的原型对象和构造函数,示例代码如下:
// * 通过装饰器 修改原型对象与构造函数
function addName(constructor: { new (): any }) {
constructor.prototype.name = '一碗周'
}
@addName
class Person {}
const person = new Person()
console.log(person.name) // error 类型“A”上不存在属性“name”
在上述代码中,我们通过addName修饰符在类Person的原型上添加了一个name属性,使得通过Person类实例化的对象均可访问name属性。但实际上这会抛出异常,解决此问题可通过类型断言方式,或定义一个同名接口并通过声明合并来解决。
示例代码如下:
function addName(constructor: { new (): any }) {
constructor.prototype.name = '一碗周'
}
@addName
class Person {}
const person = new Person()
// 1. 类型断言
// console.log((person as any).name) // 一碗周
// 2. 定义同名接口,声明合并
interface Person {
name: string
}
console.log(person.name) // 一碗周
此外,我们还可以通过装饰器重载构造函数,示例代码如下:
// * 重载构造函数
function classDecorator<T extends { new (...args: any[]): {} }>(
constructor: T,
) {
return class extends constructor {
name = '一碗周'
hobby = 'coding'
}
}
@classDecorator
class Person {
age = 18
name: string
constructor(name: string) {
this.name = name
}
}
const person = new Person('一碗周')
console.log(person)
/* 执行结果如下:
{
age: 18,
name: '一碗周',
hobby: 'coding',
}
*/
我们还可以通过装饰器工厂的方式传递参数,示例代码如下:
// 定义一个装饰器工厂
function classDecorator(_name: string) {
return function <T extends { new (...args: any[]): {} }>(constructor: T) {
return class extends constructor {
name = _name
hobby = 'coding'
}
}
}
@classDecorator('一碗周')
class Person {
age = 18
name: string
constructor(name: string) {
this.name = name
}
}
const person = new Person('一碗粥')
console.log(person)
/* 执行结果如下:
{
age: 18,
name: '一碗周',
hobby: 'coding',
}
*/
3.方法装饰器
方法装饰器用于处理类中的方法,它可以处理方法的属性描述符(关于属性描述符的详情,请参考Object.defineProperty()),也可以处理方法定义。方法装饰器在运行时同样作为函数调用,其包含三个参数。
具体如下所示:
对于静态成员,第一个参数为类的构造函数;对于实例成员,则为类的原型对象。
第二个参数为成员的名字。
第三个参数为成员的属性描述符。
值得注意的是,如果代码输出目标版本小于ES5,属性描述符将为undefined。
如下代码通过装饰器工厂定义了一个简单的方法装饰器,示例代码如下:
// 装饰器工厂
function enumerable(bool: boolean) {
/**
* 方法装饰器接受三个参数:
* 1. target:对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
* 2. propertyName:成员的名字
* 3. descriptor:属性描述符,其类型为 PropertyDescriptor
*/
return function (
target: any,
propertyName: string,
descriptor: PropertyDescriptor,
) {
// 根据传入的bool决定该方法是否可枚举
descriptor.enumerable = bool
}
}
class Info {
constructor(public name: string) {}
@enumerable(false)
getName() {
return this.name
}
}
const info = new Info('一碗周')
// 如果直接打印,该对象中不包含 getName() 方法,因为该方法是不可枚举的。
console.log(info) // { name: '一碗周' }
// 但是可以调用该方法
console.log(info.getName()) // 一碗周
在上述代码中,我们直接通过装饰器修改了类中方法的属性描述符。
如果方法装饰器返回一个值,则该值将作为方法的属性描述符对象,示例代码如下:
// 装饰器工厂
function enumerable(bool: boolean) {
return function (
target: any,
propertyName: string,
descriptor: PropertyDescriptor,
) {
return {
value: function () {
return 'Error: name is undefined'
},
enumerable: bool,
}
}
}
class Info {
constructor(public name: string) {}
@enumerable(false)
getName() {
return this.name
}
}
const info = new Info('一碗周')
console.log(info) // { name: '一碗周' }
console.log(info.getName()) // Error: name is undefined
在上述代码中,方法装饰器返回了一个对象,该对象的value属性修改了方法的定义,因此最终看到的结果为Error: name is undefined。
4.访问器装饰器
访问器装饰器即之前学习的set和get方法,前者在设置属性值时触发,后者在获取属性值时触发。
访问器装饰器同样接收三个参数,与方法装饰器相同,此处不再赘述。
示例代码如下:
function enumerable(bool: boolean) {
return function (
target: any,
propertyName: string,
descriptor: PropertyDescriptor,
) {
descriptor.enumerable = bool
}
}
class Info {
private _name: string
constructor(name: string) {
this._name = name
}
@enumerable(false)
get name() {
return this._name
}
set name(name) {
this._name = name
}
}
需要注意的是,在TypeScript中不允许同时装饰一个成员的get和set访问器。
5.属性装饰器
属性装饰器声明在属性声明之前,它包含两个参数,如下所示:
- 对于静态成员,第一个参数为类的构造函数;对于实例成员,则为类的原型对象。
- 第二个参数为成员的名字。
示例代码如下:
function printPropertyName(target: any, propertyName: string) {
console.log(propertyName)
}
class Info {
@printPropertyName
name: string
@printPropertyName
age: number
constructor(name: string, age: number) {
this.name = name
this.age = age
}
}
new Info('一碗周', 18)
执行结果如下:
name age
6.参数装饰器
参数装饰器具有三个参数,具体如下:
- 对于静态成员,第一个参数为类的构造函数;对于实例成员,则为类的原型对象。
- 第二个参数为成员的名字。
- 第三个参数为参数在函数参数列表中的索引。
参数装饰器用于监视方法参数是否被传入,其返回值会被忽略。
示例代码如下:
function required(target: any, propertyName: string, index: number) {
console.log(`修饰的是${propertyName}的第${index + 1}个参数`)
}
class Info {
name: string = '一碗周'
age: number = 18
getInfo(prefix: string, @required infoType: string): any {
return prefix + ' ' + this[infoType]
}
}
interface Info {
[key: string]: string | number | Function
}
const info = new Info()
info.getInfo('', 'age') // 修饰的是getInfo的第2个参数
这里我们在getInfo方法的第二个参数之前使用参数装饰器,从而可以在装饰器中获取相关信息。
到此这篇关于TypeScript 装饰器定义的文章就介绍到这了,更多相关TypeScript 装饰器内容请搜索本站以前的文章或继续浏览下面的相关文章希望大家以后多多支持本站!