TypeScript 装饰器

71 阅读5分钟

一、简介

  1. 装饰器本质是一种特殊的函数,它可以对:类、属性、方法、参数进行扩展,同时能让代码更简洁
  2. 装饰器自2015年在ECMAScript-6中被提出到现在,已接近10年
  3. 截止目前,装饰器依然是实验性特性,需要开发者手动调整配置,来开启装饰器支持。
  4. 装饰器有5种
    • 类装饰器
    • 属性装饰器
    • 方法装饰器
    • 访问器装饰器
    • 参数装饰器

备注:虽然TypeScript5.0中可以直接使用类装饰器,但为了确保其他装饰器可用,现阶段使用仍建议使用experimentalDecorators 配置开启装饰器支持,而且不排除在未来的版本中,官方会进一步调整装饰器的相关语法!

二、类装饰器

1. 基本语法

类装饰器是一个应用在类声明上的函数,可以为类添加额外的功能,或添加额外的逻辑。

/*
Demo函数会在Person类定义时执行
参数说明:
    target:参数是被装饰的类,即:Person
*/
function Demo(target:Function){
    console.log(target)
}

// 使用装饰器
@Demo
class Person {}

2. 应用举例

需求:定义一个装饰器,实现Person实例调用toString时返回JSON.stringify的执行结果。

//使用装饰器重写toString写法 + 封闭其原型对象
function CustomString(target:Function){
    // 向被装饰类的原型上添加自定义的toString方法
    target.prototype.toString = function () {
        return JSON.stringify(this)
    }
    //封闭其原型对象,禁止随意操作其原型对象
    Object.seal(target.prototype)
}

// 使用 CustomString装饰器
@CustomString
class Person {
    constructor(public name:string,public age:number){}
    speak() {
        console.log('你好呀!')
    }
}

// 测试代码如下
let p1 = new Person('张三'18)
// 输出: {'name':'张三',‘age’:18}
console.log(p1.toString())

3. 关于返回值

类装饰器有返回值:若类装饰器返回一个新的类,那这个新类将替换掉被装饰的类 类装器无返回值:若类装饰器无返回值或返回undefined,那被装饰的类不会被替换。

function demo(target:Function) {
    // 装饰器有返回值时,该返回值会替换掉被装饰的类
    return class {
        test(){
            console.log(200)
            console.log(300)
            console.log(400)
        }
    }
}

@demo
class Person {
    test(){
        console.log(100)
    }
}
console.log(Person)

4. 关于构造类型

在TypeScript中,Function 类型所表示的范围十分广泛,包括:普通函数、箭头函数、方法等等。但并非Function 类型的函数都可以被new关键字实例化。例如箭头函数是不能被实例化的,那么TypeScript中如何声明一个构造函数呢?有以下两种方式:

仅声明构造类型
/*
   。new 表示:该类型是可以用new操作符调用
   。 ...args 表示:构造器可以接受【任意数量】的参数
   。 any[] 表示:构造器可以接受【任意类型】的参数
   。 {} 表示:返回类型是对象(非null、非undefined的对象)
*/

//定义Constructor 类型,其含义是构造类型
type Constructor = new (...args:any[]) => {}

function test(fn:Constructor){}
class Person{}
test(Person)
声明构造类型 + 指定静态属性
//定义Constructor 类型,且包含一个静态属性wife
type Constructor = {
    new (...args:any[]):{} // 构造签名
    wife:string;// wife属性
} 

function test(fn:Constructor){}
class Person{
    static wife = 'asd'
}
test(Person)

5. 替换被装饰的类

对于高级一些的装饰器,不仅仅是覆盖一个原型上的方法,还要有更多功能,例如添加新的方法和状态。

设计一个LogTime装饰器,可以给实例添加一个属性,用于记录实例对象的创建时间,再添加一个方法用于读取创建时间。

interface Person {
    getTime():void
}

//自定义类型Class
type Constructor = new (...args:any[]) => {}

//创建一个装饰器,为类添加日志功能和创建时间
function LogTime<T extends Constructor>(target:T){
    return class extends target {
        createdTime:Date;
        constructor(...args:any[]){
            super(...args);
            this.createTime = new Date(); //记录对象创建时间
        }
        getTime(){
            return '该对象创建时间为:${this.createTime}'
        }
    }
}
@LogTime
class Person {
    name: string
    age: number
    constructor(name:string,age:number){
        this.name = name
        this.age = age
    }
    speak(){
        console.log('你好呀!')
    }
}
const p1 = new Person('张三'18)
cosole.log(p1.getTime())

三、装饰器工厂

装饰器工厂是一个返回装饰器函数的函数,可以为装饰器添加参数,可以更灵活地控制控制器的行为

需求:定义一个LogInfo类装饰器工厂,实现Person实例可以调用到introduce方法,且introduce中输出内容的次数,由LogInfo接收的参数决定

interface Person {
    introduce: () => void
}

// 定义一个装饰器工厂LogInfo,它接受一个参数n,返回一个类装饰器
function LogInfo(n:number){
    // 装饰器函数,target是被装饰的类
    return function(target:Function){
        target.prototype.introduce = function() {
            for (let i = 0;i < n ;i++){
                console.log(`我的名字:${this.name},我的年龄:${this.age}`)
            }
        }
    }
}
@LogInfo(5)
class Person {
    constructor(
        public name:string,
        public age:number
    ){}
    speak() {
        cosole.log('你好呀!')
    }
}
let p1 = new Person('张三',18)
//console.log(p1) // 打印p1是:_classThis,转换的JS版本比较旧时,会出现,不必纠结
p1.speak()

四、装饰器组合

装饰器可以组合使用,执行顺序为:先 【由上到下】执行所有的装饰器工厂,一次获取到装饰器,然后 再【由下到上】执行所有的装饰器。

装饰器组合 - 执行顺序

//装饰器
function test1(target:Function){
    console.log('tes1')
}

//装饰器工厂
function test2(){
    console.log('tes2工厂')
    return function (target:Function){
        console.log('tes2')
    }
}
//装饰器工厂
function test3(){
    console.log('test3工厂')
    return function (target:Function){
        console.log('test3')
    }
}
//装饰器
function test4(target:Function){
    console.log('tes4')
}

@test1
@test2()
@test3()
@test4()
class Person{} 

装饰器组合 - 应用

// 自定义类型Class
type Constructor = new (...args:any[]) = {}

interface Person {
    introduce():void
    getTime():void
}
// 使用装饰器重写toString方法 + 封闭其原型对象
function customToString(target:Function) {
    // 向被装饰类的原型上添加自定义的toString方法
    target.prototype.toString = function(){
        return JSON.stringify(this)
    }
    //封闭其原型对象,禁止随意操作其原型对象
    Object.seal(target.prototype)
}

// 创建一个装饰器,为类添加日志功能和创建时间
function LogTime<T extends Constructor>(target:T){
    return class extends target {
        createdTime:Date;
        constructor(...args:any[]){
            super(...args);
            this.createTime = new Date(); //记录对象创建时间
        }
        getTime(){
            return '该对象创建时间为:${this.createTime}'
        }
    }
}

// 定义一个装饰器工厂LogInfo,它接受一个参数n,返回一个类装饰器
function LogInfo(n:number){
    // 装饰器函数,target是被装饰的类
    return function(target:Function){
        target.prototype.introduce = function() {
            for (let i = 0;i < n ;i++){
                console.log(`我的名字:${this.name},我的年龄:${this.age}`)
            }
        }
    }
}

@customToString
@LogInfo(3)
@LogTime
class Person {
    constructor(
        public name:string,
        public age:number
    ){}
    speak() {
        cosole.log('你好呀!')
    }
}

const p1 = new Person('张三',18)
console.log(p1.toString())
p1.introduce()
console.log(p1.getTime())

五、属性装饰器

1. 基本语法

/*
    参数说明:
    。 target:对于静态属性来说值是类(这里对应着Person类),对于实例属性来说值是类的原型对象。
    。 propertyKey:属性名
*/

function Demo(target:object,propertyKey:string){
    console.log(target,propertyKey)
}
class Person {
    @Demo name:string
    @Demo age:number
    @Demo static school:string
    
    constuctor(name:string,age:number){
        this.name = name
        this.age = age
    }
}

2. 关于属性遮蔽

class Person {
    name:string
    age:number
    
    constuctor(name:string,age:number){
        this.name = name
        this.age = age
    }
}

let value = 99

// 使用defineProperty给Person原型添加age属性,并配置对应的get与set
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

3. 应用举例

定义一个State属性装饰器,来监视属性和修改

function State(target:object,propertyKey:string){
    // 存储属性的内部值
    let key = `__${propertyKey}`;
    
    // 使用Object.defineproperty 替换类的原始属性
    // 重新定义属性,使其使用自定义的getter 和 setter
    Object.defineProperty(target,propertyKey,{
        get(){
            return this[key]
        },
        set(newVal:string){
            console.log(`${propertyKey}的最新值为:${newVal}`)
            this[key] = newVal
        }
        enumerable:true,
        configurable:true,
    })

}

class Person {
    name: string
    @State age:number
    constructor(name:string,age:number){
        this.name = name
        this.age = age
    }
}

const p1 = new Person('张三',18)

六、方法装饰器

1. 基本语法

/*
    参数说明:
    target:对于静态方法来说值是类,对于实例方法来说值是原型对象
    propertyKey:方法的名称
    descriptor:方法的描述对象,其中value属性是被装饰的方法
*/

function Demo(target:object,propertyKey:string,descriptor:PropertyDescriptor){
    console.log(target)
    console.log(propertyKey)
    console.log(descriptor)
} 
class Person {
    constructor(
        public name:string,
        public age:number
    ){}
    // Demo装饰实例方法
    @Demo speak(){
         console.log(`我的名字:${this.name},我的年龄:${this.age}`)
    }
    // Demo装饰静态方法
    @Demo static isAdult(age:number){
        return age >= 18
    }
}

const p1 = new Person('张三',18)
p1.speak()

2. 应用举例

  1. 定义一个Logger方法装饰器,用于在方法执行前和执行后,均追加一些额外逻辑
  2. 定义一个Validate方法装饰器,用于验证数据。
function Logger(target:object,propertyKey:string,descriptor:PropertyDescriptor){
    // 保存原始方法
    const original = descriptor.value;
    // 替换原始方法
    descriptor.value = function(...args:any[]){
        console.log(`${propertyKey}开始执行。。。。。。`)
        const result = original.call(this,...args)
        console.log(`${propertyKey}开始完毕。。。。。。`)
        return result
    }
}
function Validate(maxValue:number){
    return function (target:object,propertyKey:string,descriptor:PropertyDescriptor){
       // 保存原始方法
        const original = descriptor.value;
        // 替换原始方法
        descriptor.value = function(...args:any[]){
            //自定义的验证逻辑
            if (args[0] > maxValue){
                throw new Error('年龄非法!')
            }
            // 如果所有参数都符合要求,则调用原始方法
            return original.apply(this,args)
        } 
    }
}

class Person {
    constructor(
        public name:string,
        public age:number  
    ){}
    @Logger speak(){
        console.log(`你好,我的名字:${this.name},我的年龄:${this.age}`)
    }
    @Validate(120)
    static isAult(age:number){
        return age >= 18
    }
}
const p1 = new Person('张三',18)
p1.speak()
console.log(Person.isAdult(100))

七、访问器装饰器

1. 基本语法

/*
    参数说明
    target:
        对于实例访问器来说值是【所属类的原始对象】
        对于静态访问器来说值是【所属类】
    propertyKey:访问器的名称,
    descriptor:描述对象
*/
function Demo(target:object,propertyKey:string,descriptor:PropertyDescriptor){
    console.log(target)
    console.log(propertyKey)
    console.log(descriptor)
}
class Person {
    @Demo
    get address(){
        return '深圳南山科技园'
    }
    @Demo
    static get country(){
        return '中国'
    }
}

2. 应用举例

对Weather类的temp属性的set访问器进行限制,设置的最低温度 -50,最高温度 50

function RangeValidate(min:number,max:number){
   return function(target:object,propertyKey:string,descriptor:PropertyDescriptor){
     // 保存原始方法
     const originalSetter = descriptor.value;
     
     //重写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 = _temp
    }
    get temp(){
        return this._temp
    }
}

const w1 = new Weather(25)
console.log(w1)
w1.temp = 67
console.log(w1)

八、参数装饰器

/*
   参数说明
    target:
        如果修饰的是【实例方法】的参数,target是类的【原型对象】
        如果修饰的是【静态方法】的参数,target是【类】
    propertyKey:参数所在的方法的名称
    parameterIndex:参数在函数参数列表中你的索引,从0开始。
*/
function Demo(target:object,propertyKey:string,parameterIndex:number){
    console.log(target)
    console.log(propertyKey)
    console.log(parameterIndex)
}

// 类定义
class Peron {
    constructor(public name:string)
    speak(@Demo message1:any,message2:any){
         console.log(`${this.name}想对说:${this.message1},${this.message2}`)
    }
    
    
}