如何传入比较器_typescript 13 装饰器

dd919514c0a2a85c964baf87fa7f14ed.png

知识导向

一、基础

  1. 装饰器定义
  2. 装饰器工厂
  3. 装饰器组合
  4. 装饰器求值

二、类装饰器

三、方法装饰器

四、访问器装饰器

五、属性装饰器

六、参数装饰器

一、基础

随着TypeScript和ES6里引入了类,在一些场景下我们需要额外的特性来支持标注或修改类及其成员。 装饰器(Decorators)为我们在类的声明及成员上通过元编程语法添加标注提供了一种方式。 Javascript里的装饰器目前处在 建议征集的第二阶段,但在TypeScript里已做为一项实验性特性予以支持。

注意 装饰器是一项实验性特性,在未来的版本中可能会发生改变。

若要启用实验性的装饰器特性,你必须在命令行或tsconfig.json里启用experimentalDecorators编译器选项:

tsconfig.json:

{
    "compilerOptions": {
        "target": "ES5",
        "experimentalDecorators": true
    }
}
  1. 装饰器定义

装饰器是一种特殊类型的声明,它能够被附加到类声明,方法,访问符,属性或参数上。 装饰器使用@setName这种形式,装饰器是一个函数setName(),它会在运行时被调用,被装饰的声明信息做为参数传入。

装饰器@setName必须紧挨放置于要修饰的内容前面,装饰器定义的方法必须接受一个参数 。

例如:

function setName(target: new() => any) {
    console.log(target)
}

@setName
class Prop {
    constructor() { }
}

这里的@setName就是一个类装饰器(下面会讲到),运行时会调用setName()方法,传入的target参数就是类的构造函数。

2. 装饰器工厂

如果我们要定制一个修饰器如何应用到一个声明上,我们得写一个装饰器工厂函数。装饰器工厂就是一个简单的函数(可以传入参数),它返回一个表达式,以供装饰器在运行时调用。

我们可以通过下面的方式来写一个装饰器工厂函数:

function setName(name: string) {
    console.log(`这里是一个${name}定义的装饰器工厂`);
    return (target: new() => any) => {
        console.log('这里是返回的表达式,是一个装饰器');
    }
}
@setName('cyang')
class Prop {
    constructor() {}
}

//结果:
//这里是一个cyang定义的装饰器工厂
//这里是返回的表达式,是一个装饰器

通过这个例子我们可以看出装饰器工厂与装饰器运行时的顺序。

3. 装饰器组合

多个装饰器可以同时应用到一个声明上,就像下面的示例:

function setName(target: new() => any) {
    console.log('装饰器setName')
}
function setAge(target: new() => any) {
    console.log('装饰器setAge')
}

@setName
@setAge
class Prop {
    constructor() { }
}

多个装饰器调用顺序是由上至下依次对装饰器表达式求值,求值的结果会被当作函数,再由下至上依次调用。

当我们同时使用装饰器工厂和装饰器时,求值顺序会发生变化:

function setName() {
    console.log('调用setNanme装饰器工厂');
    return (target: any) => {
        console.log('调用setName装饰器');
    }
}
function setAge() {
    console.log('调用setAge装饰器工厂');
    return (target: any) => {
        console.log('调用setAge装饰器');
    }
}
function setAddress(target: any) {
    console.log('调用setAddress装饰器');
}

@setName()
@setAge()
@setAddress
class Prop {
    constructor() {}
}

调用结果:

调用setNanme装饰器工厂
调用setAge装饰器工厂
调用setAddress装饰器
调用setAge装饰器
调用setName装饰器
先调用装饰器工厂并返回相对应的装饰器,再从后至前调用装饰器(包括返回的装饰器表达式),例如:
1.@setName()
2.@setAge()
3.@setAddress
4.@setSex

调用顺序为1->2->4->3->2返回的装饰器->1返回的装饰器

4. 装饰器求值

类中不同声明上的装饰器将按以下规定的顺序应用:

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

二、类装饰器

类装饰器在类声明之前被声明(紧靠着类声明)。 类装饰器应用于类构造函数,可以用来监视,修改或替换类定义。 类装饰器不能用在声明文件中( .d.ts),也不能用在任何外部上下文中(比如declare的类)。

类装饰器表达式会在运行时当作函数被调用,类的构造函数作为其唯一的参数。

如果类装饰器返回一个值,它会使用提供的构造函数来替换类的声明。

注意 如果你要返回一个新的构造函数,你必须注意处理好原来的原型链。 在运行时的装饰器调用逻辑中 不会为你做这些。

下面是使用类装饰器(@sealed)的例子,应用在Greeter类:

@sealed
class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        return "Hello, " + this.greeting;
    }
}

我们可以这样定义@sealed装饰器:

function sealed(constructor: Function) {
    Object.seal(constructor);
    Object.seal(constructor.prototype);
}

@sealed被执行的时候,它将密封此类的构造函数和原型。(注:参见Object.seal)

下面是一个重载构造函数的例子:

function classDecorator<T extends {new(...args:any[]):{}}>(constructor:T) {
    return class extends constructor {
        newProperty = "new property";
        hello = "override";
    }
}

@classDecorator
class Greeter {
    property = "property";
    hello: string;
    constructor(m: string) {
        this.hello = m;
    }
}

console.log(new Greeter("world"));

三、方法装饰器

方法装饰器声明在一个方法的声明之前(紧靠着方法声明)。 它会被应用到方法的 属性描述符上,可以用来监视,修改或者替换方法定义。 方法装饰器不能用在声明文件( .d.ts),重载或者任何外部上下文(比如declare的类)中。

方法装饰器表达式会在运行时当作函数被调用,传入下列3个参数:

  1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  2. 成员的名字。
  3. 成员的属性描述符
注意 如果代码输出目标版本小于 ES5属性描述符将会是 undefined

这里补充一个属性描述符的知识:Object.defineProperty

它接收三个参数,

Object.defineProperty =(需要绑定的对象,'key值',{ 
    value: '值',
    configurable: 是否可配置,
    writeable: 是否可写,
    enumerable: 是否可枚举,
})

例如:

let Obj = {}
Object.defineProperty(Obj, 'num', {
    value: 2,
    configurable: false,
    writable: true,
    enumerable: false,
})
console.log(Obj);
//结果 { num: 2 }

这样能够进行更精准的控制对象属性。

下面是一个方法装饰器(@enumerable)的例子,应用于Greeter类的方法上:

class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }

    @enumerable(false)
    greet() {
        return "Hello, " + this.greeting;
    }
}

我们可以用下面的函数声明来定义@enumerable装饰器:

function enumerable(value: boolean) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        descriptor.enumerable = value;
    };
}

这里的@enumerable(false)是一个装饰器工厂。 当装饰器 @enumerable(false)被调用时,它会修改属性描述符的enumerable属性。

四、访问器装饰器

访问器装饰器声明在一个访问器的声明之前(紧靠着访问器声明)。 访问器装饰器应用于访问器的 属性描述符并且可以用来监视,修改或替换一个访问器的定义。 访问器装饰器不能用在声明文件中(.d.ts),或者任何外部上下文(比如 declare的类)里。

注意 TypeScript不允许同时装饰一个成员的 getset访问器。取而代之的是,一个成员的所有装饰的必须应用在文档顺序的第一个访问器上。这是因为,在装饰器应用于一个 属性描述符时,它联合了 getset访问器,而不是分开声明的。

访问器装饰器表达式会在运行时当作函数被调用,传入下列3个参数:

  1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  2. 成员的名字。
  3. 成员的属性描述符
注意 如果代码输出目标版本小于 ES5Property Descriptor将会是 undefined

如果访问器装饰器返回一个值,它会被用作方法的属性描述符

注意 如果代码输出目标版本小于 ES5返回值会被忽略。

下面是使用了访问器装饰器(@configurable)的例子,应用于Point类的成员上:

class Point {
    private _x: number;
    private _y: number;
    constructor(x: number, y: number) {
        this._x = x;
        this._y = y;
    }

    @configurable(false)
    get x() { return this._x; }

    @configurable(false)
    get y() { return this._y; }
}

我们可以通过如下函数声明来定义@configurable装饰器:

function configurable(value: boolean) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        descriptor.configurable = value;
    };
}

五、属性装饰器

属性装饰器声明在一个属性声明之前(紧靠着属性声明)。 属性装饰器不能用在声明文件中(.d.ts),或者任何外部上下文(比如 declare的类)里。

属性装饰器表达式会在运行时当作函数被调用,传入下列2个参数:

  1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  2. 成员的名字。
注意 属性描述符不会做为参数传入属性装饰器,这与TypeScript是如何初始化属性装饰器的有关。 因为目前没有办法在定义一个原型对象的成员时描述一个实例属性,并且没办法监视或修改一个属性的初始化方法。返回值也会被忽略。因此,属性描述符只能用来监视类中是否声明了某个名字的属性。

我们可以用它来记录这个属性的元数据,如下例所示:

class Greeter {
    @format("Hello, %s")
    greeting: string;

    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        let formatString = getFormat(this, "greeting");
        return formatString.replace("%s", this.greeting);
    }
}

然后定义@format装饰器和getFormat函数:

import "reflect-metadata";

const formatMetadataKey = Symbol("format");

function format(formatString: string) {
    return Reflect.metadata(formatMetadataKey, formatString);
}

function getFormat(target: any, propertyKey: string) {
    return Reflect.getMetadata(formatMetadataKey, target, propertyKey);
}

这个@format("Hello, %s")装饰器是个 装饰器工厂。 当 @format("Hello, %s")被调用时,它添加一条这个属性的元数据,通过reflect-metadata库里的Reflect.metadata函数。 当 getFormat被调用时,它读取格式的元数据。

注意 这个例子需要使用 reflect-metadata库。 查看 元数据了解 reflect-metadata库更详细的信息。

六、参数装饰器

参数装饰器声明在一个参数声明之前(紧靠着参数声明)。 参数装饰器应用于类构造函数或方法声明。 参数装饰器不能用在声明文件(.d.ts),重载或其它外部上下文(比如 declare的类)里。

参数装饰器表达式会在运行时当作函数被调用,传入下列3个参数:

  1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  2. 成员的名字。
  3. 参数在函数参数列表中的索引。
注意 参数装饰器只能用来监视一个方法的参数是否被传入。

参数装饰器的返回值会被忽略。

function required(target: any, propertName: string, index: number) {
    console.log(`修饰的是${propertName}的第${index+1}个参数`);
}
class Greeter {
    public name: string = 'cyang'
    public age: number = 18
    public getInfo(@required prefix: string, infoType: string): any {
        return prefix + '' + this[infoType]
    }
}
interface Greeter {
    [key: string]: string | number | Function
}
const greeter = new Greeter()
console.log(greeter.getInfo('年龄', 'age'));

结果:

修饰的是getInfo的第1个参数
年龄18

装饰器定义的方法会在运行时被调用。

[1]

参考

  1. ^装饰器大部分例子参照typescript中文官网列出 https://www.tslang.cn/docs/handbook/decorators.html
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值