TS文档学习 --- Decorators

415 阅读4分钟

本篇翻译整理自 TypeScript Handbook 中 「Decorators」 章节。

随着TypeScript和ES6里引入了类,在一些场景下我们需要额外的特性来支持标注或修改类及其成员。

装饰器(Decorators)为我们在类的声明及成员上通过元编程语法添加标注提供了一种方式。

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

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

tsconfig.json

{
    "compilerOptions": {
        "target": "ES5",
        "experimentalDecorators": true
    }
}

装饰器

_装饰器_是一种特殊类型的声明,它能够被附加到类声明方法访问符属性参数上。

装饰器使用@expression这种形式,expression求值后必须为一个函数,

它会在运行时被调用,被装饰的声明信息做为参数传入。

类装饰器

_类装饰器_在类声明之前被声明(紧靠着类声明)

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

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;
    }
}

const gretter = new Greeter("world")
console.log(gretter.hello) // => override

方法装饰器

_方法装饰器_声明在一个方法的声明之前(紧靠着方法声明)

它会被应用到方法的_属性描述符_上,可以用来监视,修改或者替换方法定义。

方法装饰器不能用在声明文件(.d.ts),重载或者任何外部上下文(比如declare的类)中。

function enumerable(value: boolean) {
  	/*
  		target === 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
  		key === 成员的名字
  		descriptor === 成员的属性描述符(仅在target大于ES5的时候有效)
  		如果方法装饰器返回一个值,它会被用作方法的属性描述符(仅在target大于ES5的时候有效)
  	*/
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        console.log('target', target)
        console.log('key', propertyKey)
        console.log('descriptor', descriptor)
        descriptor.enumerable = value;
    };
}

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

  	// 在调用方法装饰器的时候,调用enumerable函数,并传递参数为false --- 返回值才是真正使用的装饰器
    @enumerable(false)
    greet() {
        return "Hello, " + this.greeting;
    }
}

访问器装饰器

_访问器装饰器_声明在一个访问器的声明之前(紧靠着访问器声明)

访问器装饰器应用于访问器的_属性描述符_并且可以用来监视,修改或替换一个访问器的定义

访问器装饰器不能用在声明文件中(.d.ts),或者任何外部上下文(比如declare的类)里

访问器装饰器表达式会在运行时当作函数被调用,传入的参数和方法装饰器一致,

如果访问器装饰器同样返回了一个值,其效果也和方法装饰器保持一致

注意  TypeScript不允许同时装饰一个成员的getset访问器。

取而代之的是,一个成员的所有装饰的必须应用在文档顺序的第一个访问器上。

这是因为,在装饰器应用于一个_属性描述符_时,它联合了getset访问器,而不是分开声明的。

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

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; }
}

属性装饰器

_属性装饰器_声明在一个属性声明之前(紧靠着属性声明)。

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

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

  1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  2. 成员的名字。
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);
}

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);
    }
}

参数装饰器

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

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

  1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  2. 成员的名字。
  3. 参数在函数参数列表中的索引。

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

class Greeter {
  greeting: string;

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

  @validate
  greet(@required name: string) {
    return "Hello " + name + ", " + this.greeting;
  }
}
import "reflect-metadata";

const requiredMetadataKey = Symbol("required");

// @required装饰器添加了元数据实体把参数标记为必需的
function required(target: Object, propertyKey: string | symbol, parameterIndex: number) {
    let existingRequiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyKey) || [];
    existingRequiredParameters.push(parameterIndex);
    Reflect.defineMetadata(requiredMetadataKey, existingRequiredParameters, target, propertyKey);
}

// @validate进行参数校验
function validate(target: any, propertyName: string, descriptor: TypedPropertyDescriptor<Function>) {
    let method = descriptor.value;
    descriptor.value = function () {
        let requiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyName);
        if (requiredParameters) {
            for (let parameterIndex of requiredParameters) {
                if (parameterIndex >= arguments.length || arguments[parameterIndex] === undefined) {
                    throw new Error("Missing required argument.");
                }
            }
        }

        return method.apply(this, arguments);
    }
}