一、简介
- 装饰器本质是一种特殊的函数它可以对:类、属性、方法、参数进行扩展,同时能让代码更简介。
- 装饰器自 2015 年在ECMAScript-6 中被提出到现在,已将近10年。
- 截止目前,装饰器依然是实验性特性,需要开发者手动调整配置,来开启装饰器支持。
- 装饰器有5种:
- 类装饰器
- 属性装饰器
- 方法装饰器
- 访问器装饰器
- 参数装饰器
备注:虽然 Typescript5.0 中可以直接使用 类装饰器 ,但为了确保其他装饰器可用,现阶段使用时仍建议使用 experimentalDecorators 配置来开启装饰器支持,而且不排除在未来的版本中,官方会进一步调整装饰器的相关语法!
参考:Announcing TypeScript 5.0 RC - TypeScript
二、类装饰器
1、基础语法
类装饰器是一个应用在类声明上的函数,可以为类添加额外的功能,或添加额外的逻辑。
第一步:开启支持装饰器
第二步:入门基础语法
// 装饰器函数
/*
Demo函数会在 Person类 定义时执行
参数说明:
o target参数是被装饰的类,即:Person
*/
function Demo(target: Function) {
console.log(target);
}
@Demo // 等价于 执行了 Demo 函数 ,还将 Person 传进去调用了 Demo(Person)
class Person{
constructor(public name:string, public age:number){}
greet(){
console.log(`Hi, I am ${this.name}`)
}
}
2、应用举例
需求:定义一个装饰器,实现 Person 实例调用 tostring 时返回 JS0N.stringify 的执行结果。
function CustomString(target: Function) {
// 通过 target 获得属性, 添加 toString 属性
target.prototype.toString = function(){
return JSON.stringify(this)
}
// 封锁 target 属性 ,不允许更改
Object.seal(target.prototype)
}
@CustomString
class Person{
constructor(public name:string, public age:number){}
greet(){
console.log(`Hi, I am ${this.name}`)
}
}
const p1 = new Person('Angindem', 30)
console.log(p1.toString());
/*
// @ts-ignore : 忽略类型检查
*/
interface Person{
x:number
}
Person.prototype.x = 99
3、关于返回值
类装饰器有返回值:若类装饰器返回一个新的类,那这个新类将替换掉被装饰的类。
类装饰器无返回值:若类装饰器无返回值或返回 undefined ,那被装饰的类不会被替换。
function Demo(target:Function) {
return class {
test() {
console.log('200');
console.log('300');
console.log('400');
}
}
}
@Demo
class Person{
test() {
console.log('test');
}
}
console.log(Person);
4、关于构造类型
在 TypeScript 中, Function 类型所表示的范围十分广泛,包括:普通函数、箭头函数、方法等等但并非 Function 类型的函数都可以被 new 关键字实例化,例如箭头函数是不能被实例化的,那么TypeScript 中概如何声明一个构造类型呢?有以下两种方式:
仅声明构造类型
/*
new 表示:该类型是可以用new操作符调用。
...args 表示:构造器可以接受【任意数量】的参数。
any[] 表示:构造器可以接受【任意类型】的参数。
{} 表示:返回类型是对象(非nu11、非undefined的对象)。
*/
type Constructor = new (...args: any[]) => {};
// 需求是 fn 的是一个类
function test(fn: Function) {
}
const Person = () => {
}
test(Person)
声明构造类型+指定静态属性
/*
new 表示:该类型是可以用new操作符调用。
...args 表示:构造器可以接受【任意数量】的参数。
any[] 表示:构造器可以接受【任意类型】的参数。
{} 表示:返回类型是对象(非nu11、非undefined的对象)。
*/
type Constructor = {
new(...args: any[]): {};
wife: string;
}
// 需求是 fn 的是一个类
function test(fn: Constructor) {
}
class Person {
static wife:string
}
test(Person)
5、替换被装饰的类
对于高级一些的装饰器,不仅仅是覆盖一个原型上的方法,还要有更多功能,例如添加新的方法和状态。
需求:设计一个 LogTime 装饰器,可以给实例添加一个属性,用于记录实例对象的创建时间,再添加一个方法用于读取创建时间。
// 声明构造器 可以 new 的 任意数量 ,任意类型 ,返回值 非 null 和 undefined
type Constructor = new (...args: any[]) => {}
// 自动合并,为 class 类里面声明我们新添加的方法
interface Person{
getTime():void
}
// <T extends Constructor> 表示我们的 泛型 target 类 必须有一个 构造器
// class extends target 新的类 保留原有的类基础上 ,我们添加 getTime 方法,以及构造时新增记录构造时间
function LogTime<T extends Constructor>(target:T) {
return class extends target{
createdTime: Date
constructor(...args: any[]) {
super(...args)
this.createdTime = new Date()
}
getTime():string{
return `This instance was created at ${this.createdTime}`
}
}
}
@LogTime
class Person{
constructor(public name:string, public age:number){}
speak():void{
console.log(`I am ${this.name} and I am ${this.age} years old.`)
}
}
const p1 = new Person('Angindem', 30)
console.log(p1.getTime());
三、装饰器工厂
装饰器工厂是一个返回装饰器函数的函数,可以为装饰器添加参数,可以更灵活地控制装饰器的行为。
需求:定义一个 LogInfo 类装饰器工厂,实现 Person 实例可以调用到 introduce 方法,且 introduce 中输出内容的次数,由 LogInfo 接收的参数决定
interface Person {
introduce():void
}
// 装饰器 需要 返回一个装饰器 所以我们 直接 @LogInfo(3) ,但是 缺少了 target 类值
// 所以我们可以 嵌套 一个函数,最后返回装饰器. 整个过程步骤,外部的就是 装饰器工厂,最后返回的是 装饰器
function LogInfo(count:number) {
return (target: Function) => {
target.prototype.introduce = function () {
for (let i = 0; i < count; i++) {
console.log(`I am ${this.name}, I am ${this.age} years old.`)
}
}
}
}
@LogInfo(3)
class Person{
constructor(public name: string, public age: number) {}
speak() {
console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
}
}
const p1 = new Person('Angindem', 30);
p1.introduce();
四、装饰器组合
装饰器可以组合使用,执行顺序为:先【由上到下】的执行所有的装饰器工厂,依次获取到装饰器,然后再【由下到上】执行所有的装饰器。
执行顺序(先 【由上到下】 工厂 ,后 【由下到上】 装饰器 )
// 装饰器
function test1(target: Function) {
console.log('test1');
}
// 装饰器工厂 2
function test2<T>(params?: T) {
console.log('test2工厂');
return function (target: Function) {
console.log('test2');
if (params) {console.log(params);}
}
}
// 装饰器工厂 3
function test3<T>(params?: T) {
console.log('test3工厂');
return function (target: Function) {
console.log('test3');
if (params) {console.log(params);}
}
}
// 装饰器
function test4(target: Function) {
console.log('test4');
}
// 像 洋葱模型 一样, 先 按顺序 执行工厂 ‘拨开’ ,最后由 下到上 执行装饰器
@test1
@test2()
@test3()
@test4
class Test { }
应用
type Constructor = new (...args: any[]) => {};
interface Person{
getTime(): string;
introduce(): void;
}
// 装饰器
function CustomString(target: Function) {
target.prototype.toString = function(){
return JSON.stringify(this)
}
Object.seal(target.prototype)
}
// 装饰器工厂
function LogInfo(count:number) {
return (target: Function) => {
target.prototype.introduce = function () {
for (let i = 0; i < count; i++) {
console.log(`I am ${this.name}, I am ${this.age} years old.`)
}
}
}
}
// 装饰器
function LogTime<T extends Constructor>(target:T) {
return class extends target{
createdTime: Date
constructor(...args: any[]) {
super(...args)
this.createdTime = new Date()
}
getTime():string{
return `This instance was created at ${this.createdTime}`
}
}
}
@CustomString
@LogInfo(5)
@LogTime
class Person{
constructor(public name:string, public age:number){}
speak(){
console.log(`Hi, I am ${this.name}`)
}
}
const p1 = new Person('Angindem', 30)
p1.speak();
p1.introduce();
console.log(p1.toString());
console.log(p1.getTime());
五、属性装饰器
1、基础语法
/**
参数说明:
o target:对于静态属性来说值是类,对于实例属性来说值是类的原型对象
o propertykey:属性名。
*/
function Demo(target: object, propertKey: string) {
console.log(target, propertKey);
}
class Person{
@Demo name: string
@Demo age: number
@Demo static school: string
constructor(name:string,age:number) {
this.name = name
this.age = age
}
speak(){
console.log(`Hi, I am ${this.name}`)
}
}
2、关于属性遮蔽问题
如下代码中:当构造器中的 this.age = age 试图在实例上赋值时,实际上是调用了原型上 age 属性的 set 方法。
class Person{
name: string
age: number
constructor(name:string,age:number) {
this.name = name
this.age = age
}
}
// // 这里 构造后 触发 查找一次 age 赋值,
// const p1 = new Person('Angindem', 18)
// 因为先构造原因, Object.defineProperty 上的 age 值 为 130,并没有收到赋值 18
// let value = 130
// Object.defineProperty(Person.prototype, 'age', {
// get() {
// return value
// },
// set(newValue) {
// value = newValue
// }
// })
let value = 130
Object.defineProperty(Person.prototype, 'age', {
get() {
return value
},
set(newValue) {
value = newValue
}
})
// 这里 构造后 触发 查找 age 赋值, 同时将 Object.defineProperty 上的 age 赋值了
const p1 = new Person('Angindem', 18)
console.log(p1);
3、应用举例
需求:定义一个 state 属性装饰器,来监视属性的修改。
function Status(target:object,propertyKey:string) {
let key = `__${propertyKey}__`
Object.defineProperty(target,propertyKey,{
get: function() {
return this[key]
},
set: function (newValue) {
if(key !== newValue) console.log(`${propertyKey} is changed from ${key} to ${newValue}`);
this[key] = newValue
},
enumerable: true, // 可枚举性: 循环遍历的时候参不参与遍历
configurable: true // 可配置性:: 是否可删除
})
}
class Person{
name: string
@Status age: number
constructor(name:string,age:number) {
this.name = name
this.age = age
}
}
const p1 = new Person('Angindem', 20)
const p2 = new Person('Angindem', 45)
p1.age = 21
console.log(p1);
console.log(p2);
六、方法装饰器
1、基础语法
/*
参数说明:
o target:对于静态方法来说值是类,对于实例方法来说值是原型对象
o propertykey:方法的名称。
o descriptor:方法的描述对象,其中value属性是被装饰的方法。
*/
function Demo(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log(target, propertyKey, descriptor);
}
class Person{
name: string
age: number
constructor(name:string,age:number) {
this.name = name
this.age = age
}
@Demo
speak() {
console.log(`${this.name} is ${this.age} years old`)
}
@Demo
static isAdult(age:number) {
return age >= 18
}
}
2、应用举例
function Logger(target: object, propertyName: string, descriptor: PropertyDescriptor) {
// 存储原始方法
const original = descriptor.value
// 替换原始方法
descriptor.value = function (...args:any[]) {
console.log(`${propertyName} 开始执行......`);
// const res = original.call(this, ...args)
const res = original.apply(this, args) // 经典面试题,注意apply和call的区别
console.log(`${propertyName} 执行完毕......`);
return res;
}
}
class Person{
name: string
age: number
constructor(name:string,age:number) {
this.name = name
this.age = age
}
@Logger speak(str:string) {
console.log(`${this.name} is ${this.age} years old ,${str}`)
}
static isAdult(age:number) {
return age >= 18
}
}
const p1 = new Person('Angindem', 18)
p1.speak('你好')
七、访问器装饰器
1、基础语法
/*
参数说明:
○ target:
1. 对于实例访问器来说值是【所属类的原型对象】。
2. 对于静态访问器来说值是【所属类】。
○ propertyKey:访问器的名称。
○ descriptor: 描述对象。
*/
function Demo(target: object, propertyKey: string, descriptor: PropertyDescriptor) {
console.log(target)
console.log(propertyKey)
console.log(descriptor)
}
class Person {
@Demo
get username() {
return 'Angindem'
}
@Demo
static get grade() {
return 'B-324'
}
}
2、应用举例
需求:对 Weather 类的 temp 属性的 set 访问器进⾏限制,设置的最低温度 -50 ,最⾼温度 50
// 装饰工厂
function RangeValidate(min: number, max: number) {
return function (target: object, propertyKey: string, descriptor: PropertyDescriptor) {
// 保存原始的 setter ⽅法,以便在后续调⽤中使⽤
const originalSetter = descriptor.set;
// 重写 setter ⽅法,加⼊范围验证逻辑
descriptor.set = function (value: number) {
// 检查设置的值是否在指定的最⼩值和最⼤值之间
if (value < min || value > max) {
// 如果值不在范围内,抛出错误
throw new Error(`${propertyKey}的值应该在 ${min} 到 ${max}之间!`);
}
// 如果值在范围内,且原始 setter ⽅法存在,则调⽤原始 setter ⽅法
if (originalSetter) {
originalSetter.call(this, value);
}
};
};
}
class Weather {
private _temp: number;
constructor(_temp: number) {
this._temp = _temp;
}
// 设置温度范围在 -50 到 50 之间
@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)
八、参数装饰器
1、基础语法
/*
参数说明:
○ target:
1.如果修饰的是【实例⽅法】的参数,target 是类的【原型对象】。
2.如果修饰的是【静态⽅法】的参数,target 是【类】。
○ propertyKey:参数所在的⽅法的名称。
○ parameterIndex: 参数在函数参数列表中的索引,从 0 开始。
*/
function Demo(target: object, propertyKey: string, parameterIndex: number) {
console.log(target)
console.log(propertyKey)
console.log(parameterIndex)
}
// 类定义
class Person {
constructor(public name: string) { }
speak(@Demo message1: any, mesage2: any) {
console.log(`${this.name}想对说:${message1},${mesage2}`);
}
}
2、应用举例
需求:定义⽅法装饰器 Validate ,同时搭配参数装饰器 NotNumber ,来对 speak ⽅法的参数类型进⾏限制。
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: PropertyDescriptor) {
const method = descriptor.value;
descriptor.value = function (...args: any[]) {
// 获取被标记为不能为空的参数索引列表
const notNumberArr: number[] = target[`__notNumber_${propertyKey}`] || [];
// 检查参数是否为 null 或 undefined
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);