欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

javascript 装饰器

最编程 2024-06-10 15:33:21
...

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

装饰器介绍

  • 开发中存在一些场景,需要给类或者类型的成员添加一下修改。装饰器就提供了一种为类和类的成员添加额外功能或修改的方法
  • 根据使用位置的不同,我们可以把装饰器分为类装饰器、方法装饰器、访问器装饰器、属性装饰器、和参数装饰器,它们分别可以作用在类声明、方法、存取器、属性或者参数上。
  • 使用形式为@expression
  • 装饰器使用时会被当成函数调用,不同类型的装饰器参数有些许不同

使用装饰器

如果要对装饰器开启支持有两种方法

  1. 需要在tsconfig.json中开启
{
    "conpilerOptions":{
        "target":"ES5",
        "experimentalDecorators":true
    }
}

需要注意的是,如果target小于ES5,装饰器也能使用,但是在有些装饰器函数中,descriptor参数会不存在

  1. 可以在命令行中开启experimentalDecorators
    tsc --target ES5 --experimentalDecorators

类装饰器

  • 类装饰器用于类的声明之前
  • 参数:被装饰类的构造函数
  • 返回:可以返回一个新的构造函数替换类型声明

以下例子把 extendsAnimal装饰器用在了Animal上 其中装饰器函数的参数target就是[class Animal] 它扩展了Animal的功能,给出了一个默认的sex为male,age比参数多1岁

function extendsAnimal(target:typeof Animal){
    console.log(target === Animal.prototype.constructor)
    return class extends target{
        sex = 'male'
        age: number
        constructor(name:string, age:number){
            super(name, age)
            this.age += 1
        }
    }
}

@extendsAnimal
class Animal{
    constructor(public name:string, public age:number){}
}
interface Tom extends Animal{
    sex:string
}
const tom = new Animal('tom',1) as Tom

console.log(tom.sex) // male
console.log(tom.age) //2

方法装饰器

  • 方法装饰器声明在一个方法的声明之前(紧靠着方法声明)。
  • 参数:
    1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
    2. 成员的名字。
    3. 成员的属性描述符。(如果ts输出目标版本小于ES5,那么属性描述符为undefined)
  • 返回:返回属性描述符descriptor或者没有返回

以下例子重写了action方法,使action可以打印年龄

function consoleAge(target: any, propertyKey:string, descriptor:PropertyDescriptor){
    const value = descriptor.value;
    if(typeof value === 'function'){
        descriptor.value = function(){
            console.log(`name:${this.name} age:${this.age} run!!`)
        }
    }
}

class Animal{
    private _address:string = ''
    constructor(public name:string, public age:number){}
    @consoleAge
    action(){
        console.log('run');
    }
}
interface Tom extends Animal{
    sex:string
}
const tom = new Animal('tom',1) as Tom

tom.action() // name:tom age:1 run!!

访问器装饰器

  • 访问器装饰器声明在一个访问器的声明之前(紧靠着访问器声明)。
  • 参数:
    1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
    2. 成员的名字。
    3. 成员的属性描述符。
  • 返回:返回属性描述符descriptor或者没有返回 以下例子通过访问器装饰器重写了get address方法,使get address可以打印名字
function formatAddress(target: any, propertyKey:string, descriptor:PropertyDescriptor){
    descriptor.get = function(){
        return `${this.name}住在${this._address}`
    }
}

class Animal{
    private _address:string = ''
    constructor(public name:string, public age:number){}
    @formatAddress
    get address(){
        return this._address;
    }
    set address(address:string){
        this._address = address
    }
}
interface Tom extends Animal{
    sex:string
}
const tom = new Animal('tom',1) as Tom
tom.address = '猫窝'
console.log(tom.address) // tom住在猫窝

属性装饰器

  • 属性装饰器声明在一个属性声明之前(紧靠着属性声明)。
  • 参数:
    1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
    2. 成员的名字。
  • 用法基本上同函数装饰器一样,只不过参数没有属性描述符

下边这个例子修改了Animal的静态属性,需要注意和上边例子的不同点是,这里装饰器修饰了静态属性,所以target变成了类本身

export function yellowColor(target:any,propertyKey:string ){
    target[propertyKey] = 'yellow'
}

class Animal{
    private _address:string = ''
    @yellowColor static color:string = 'black'
}
interface Tom extends Animal{
    sex:string
}
console.log(Animal.color); // yellow

参数装饰器

  • 参数装饰器声明在一个参数声明之前(紧靠着参数声明)。
  • 参数:
    1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
    2. 成员的名字。
    3. 参数在函数参数列表中的索引。
  • 参数装饰器的返回值会被忽略。

下边这个例子组合使用函数装饰器和参数装饰器,利用闭包存储参数必填的索引,在函数执行的时候,检查索引对应的参数是否为空,并提示

// 利用闭包保存函数缺失的必填参数
 export function validateFactory(){
    const requiredMap:Record<string, number[]|undefined> = {}
    function requiredParam(target:any, propertyKey:string, index:number){
        const indexes = [...requiredMap[propertyKey] ||[], index]
        // 保存必填参数
        requiredMap[propertyKey]  = indexes
    }
    function validateRequired(target:any, propertyKey:string, descriptor:PropertyDescriptor){
        const method = descriptor.value
        descriptor.value=function(){

            Object.keys(requiredMap).forEach(methodName=>{
                if(methodName === propertyKey){
                    requiredMap[methodName].forEach(index=>{
                        if(arguments[index]===undefined){
                            console.log(`${propertyKey} method miss required argument ${index}`)
                        }
                    })
                }
            })
            return method.apply(this, arguments)
        }
    }
    return {validateRequired, requiredParam}

}
// 获取真正的装饰器
const {validateRequired, requiredParam }= validateFactory()
class Animal{
    constructor(public name:string, public age:number){}

    @validateRequired
    say(@requiredParam message:string, @requiredParam second?:string, @requiredParam third?:string){
        console.log(`${this.name} say ${message}`)
    }
    @validateRequired
    eat(@requiredParam message:string, @requiredParam second?:string){
        console.log(`${this.name} eat ${message}`)
    }
}
interface Tom extends Animal{
    sex:string
}
const tom = new Animal('tom',1) as Tom
tom.say('hello' ,undefined,)
tom.eat('food')
// 打印如下,可以对必填字段提示
// say method miss required argument 2
// say method miss required argument 1
// tom say hello
// eat method miss required argument 1
// tom eat food

装饰器调用顺序

多个装饰器可以同时应用到一个声明上 可以书写在多行上

@log3
@log2
@log1
class Person{}

也可以书写在单行上

// 也可以这么写
@log3 @log2 @log1
class Animal{}

当多个装饰器应用于一个声明上,它们求值方式与复合函数相似。在这个模型下,当复合log3log2 log1时,复合的结果等同于log3(log2(log1(x)))。

在TypeScript里,当多个装饰器应用在一个声明上时会进行如下步骤的操作:

  1. 由上至下依次对装饰器表达式求值。
  2. 求值的结果会被当作函数,由下至上依次调用。
// 装饰器可以用在一行或多行上
function log1(target) {
    console.log('log1')
}

function log2(target) {
    console.log('log2')
}

function log3(target) {
    console.log('log3')
}

@log3
@log2
@log1
class Person{}

// 这个地方并没有new一个Person,也能打印

// 打印顺序
// log1
// log2
// log3

装饰器工厂和工厂的调用顺序

  • 装饰器工厂就是一个简单的函数,它返回一个函数,返回的这个函数实际上就是需要调用的装饰器。
  • 我们可以利用装饰器工厂传入额外参数。
  • 先自上而下调用工厂函数创建装饰器,再自下而上调用装饰器
// 装饰器工厂1
function f() {
    console.log("f(): evaluated");
    return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
        console.log("f(): called");
    }
}
// 装饰器工厂2
function g() {
    console.log("g(): evaluated");
    return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
        console.log("g(): called");
    }
}

class C {
    @f()
    @g()
    method() {
        console.log('method called')
    }
}
// 在new 之前装饰器工厂以及装饰器函数就会执行,顺序如下
// decoratorFirst
// decoratorFirst
// f(): evaluated
// g(): evaluated
// g(): called
// f(): called
const c = new C
c.method()
// 函数调用之后再执行 装饰完成的函数
// method called

不同的装饰器将按以下规定的顺序应用:

  1. 参数装饰器,然后依次是方法装饰器访问符装饰器,或属性装饰器应用到每个实例成员。
  2. 参数装饰器,然后依次是方法装饰器访问符装饰器,或属性装饰器应用到每个静态成员。
  3. 参数装饰器应用到构造函数。
  4. 类装饰器应用到类。

完整代码:github代码

参考资料:typescript装饰器