速通TypeScript装饰器

一、简介

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值