TypeScript 装饰器

TypeScript Jan 13, 2021

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

预备知识

装饰器工厂

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

例如:

function color(value: string) { // 这是一个装饰器工厂
    return function (target) { //  这是装饰器
        // do something with "target" and "value"...
    }
}

属性描述符

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

    /**
    * obj: 要定义属性的对象。
    * prop: 要定义或修改的属性的名称或 Symbol 。
    * descriptor: 要定义或修改的属性描述符。
    */
    Object.defineProperty(obj, prop, descriptor)

该方法允许精确地添加或修改对象的属性。默认的情况下,使用 Object.defineProperty() 添加的属性值是不可修改(immutable)的。

数据描述符

数据描述符是一个具有值的属性,该值可以是可写的,也可以是不可写的。

存取描述符

存取描述符是由 getter 函数和setter 函数所描述的属性。

Reflect

概述

Reflect是 ES6 为了操作对象而提供的新 API。Reflect对象的设计目的有这样几个。

  • 将 Object 对象的一些明显属于语言内部的方法(比如 Object.defineProperty),放到 Reflect 对象上。现阶段,某些方法同时在 Object和 Reflect 对象上部署,未来的新方法将只部署在 Reflect 对象上。也就是说,从 Reflect 对象上可以拿到语言内部的方法。

  • 修改某些 Object 方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc)在无法定义属性时,会抛出一个错误,而 Reflect.defineProperty(obj, name, desc) 则会返回 false。

  • 让 Object 操作都变成函数行为。某些 Object 操作是命令式,比如 name in obj 和 delete obj[name],而 Reflect.has(obj, name) 和Reflect.deleteProperty(obj, name) 让它们变成了函数行为。

  • Reflect 对象的方法与 Proxy 对象的方法一一对应,只要是 Proxy 对象的方法,就能在 Reflect 对象上找到对应的方法。这就让 Proxy 对象可以方便地调用对应的 Reflect 方法,完成默认行为,作为修改行为的基础。也就是说,不管 Proxy 怎么修改默认行为,你总可以在Reflect 上获取默认行为。

类装饰器

类装饰器应用于类构造函数,可以用来监视,修改或替换类定义

接口定义:

	declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;

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

首先,写一个最简单的装饰器:

decorator.ts :

function helloWorld(target: any) {
    console.log('hello World!')
    console.log('target :', target.toString())
}

@helloWorld
class HelloWorldClass {
    name: string = 'jerome'
    
    constructor() {
        console.log('I am constructor.')
    }

    test() {
        console.log('I am test method.')
    }
}

运行 tsc -p . 编译 ts 文件,生成 js 文件,并用 node 执行这个 js 文件:

$ node decorator/decorator.js 
hello World!
target : function HelloWorldClass() {
        this.name = 'jerome';
        console.log('I am constructor.');
    }

由此可见,装饰器在运行时就被执行,target 传递的就是 HelloWorldClass 类的构造函数,也印证了装饰器的定义中,它在运行时被调用,被装饰的声明信息作为参数传入。

接下来,我们解析一下这个 js 文件:

var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length,
        r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc,
        d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else
        for (var i = decorators.length - 1; i >= 0; i--)
            if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};

function helloWorld(target) {
    console.log('hello World!');
    console.log('target :', target.toString());
}
var HelloWorldClass = /** @class */ (function () {
    function HelloWorldClass() {
        this.name = 'jerome';
        console.log('I am constructor.');
    }
    HelloWorldClass.prototype.test = function () {
        console.log('I am test method.');
    };
    HelloWorldClass = __decorate([
        helloWorld
    ], HelloWorldClass);
    return HelloWorldClass;
}());

代码太多,让我们分几步来解析:

  • @helloWorld 类装饰器解析

    var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
        var c = arguments.length,
            r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc,
            d;
      	 // 如果原生反射可用,使用原生反射触发装饰器
        if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
        else
          	// 自右向左迭代装饰器
            for (var i = decorators.length - 1; i >= 0; i--)
              	// 如果装饰器合法,将其赋值给 d
                if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
      return c > 3 && r && Object.defineProperty(target, key, r), r;
    };
    

第一行定义了 __decorate 函数,就是通过 @helloWorld 解析出来的,用来处理装饰器的功能。

这个函数有四个参数,让我们来看看都是些什么

  
  /**
   * decorators: 数组,包含多个装饰器
   * target: 被装饰的类,即 HelloWorldClass 的构造函数
   * key: 变量名称
   * desc: 属性描述符
 */
  function (decorators, target, key, desc) {...}

c < 3 ? 为什么是 3,c 代表的是传入参数的个数,如果未传入属性描述符,r 都为 target,若传入属性描述符且不为空,则 r 为属性描述符。

通过对这段 js 的分析之后,可以简化为如下:

    var c = 2, r = target, d;
    for (var i = decorators.length - 1; i >= 0; i--)
      if (d = decorators[i]) r = d(r) || r;
      return r;

当装饰器合法时,将其赋值给 d,r = d(r) || r 相当于把 target 作为参数调用装饰器函数的结果赋值给 r, 如果 d(r) 没有返回值,返回的是原来的类 target,当有返回值的时候,返回的是 d(r) 的 return 内容。

  • HelloWorldClass 类

    从打包出的结果来看,类 HelloWorld 被解析成了一个自执行函数:

    var HelloWorldClass = /** @class */ (function () {
      function HelloWorldClass() {
          this.name = 'jerome';
          console.log('I am constructor.');
      }
      HelloWorldClass.prototype.test = function () {
          console.log('I am test method.');
      };
      HelloWorldClass = __decorate([
          helloWorld
      ], HelloWorldClass);
      return HelloWorldClass;
    }());
    

    在自执行函数中,HelloWorldClass 接收 __decorate() 函数的执行结果,相当于改变了构造函数,所以可以利用装饰器修改类的功能。

带返回值的类装饰器

上面的例子是没有返回值的装饰器函数,它返回的是原来的类,那么带返回值的装饰器函数是怎么样的呢?是否是被装饰器修改之后的类呢?让我们来一起探究一下:

按照之前的步骤,这次我们的装饰器要带有返回值,下面是一个 override 构造函数的例子:

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

运行的结果:

$ node decorator/decoratorReturn.js 
class_1 {
property: 'property',
hello: 'override',
newProperty: 'new property'
}

由代码可知,Greeter 的构造函数是对 hello 的赋值为 world,然而在使用了 @classDecorator 这个构造函数之后,hello 的值变为了 override, 说明了类装饰器应用于类构造函数,可以用来监视,修改或替换类定义。

属性装饰器

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

  • 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
  • 成员的名字。
declare type PropertyDecorator =
  (target: Object, propertyKey: string | symbol) => void;

那么,如何理解呢,让我们来看段代码:

function logParameter(target: Object, propertyName: string) {
    // 属性值
    let _val = this[propertyName];

    // 属性读取访问器
    const getter = () => {
        console.log(`Get: ${propertyName} => ${_val}`);
        return _val;
    };

    // 属性写入访问器
    const setter = newVal => {
        console.log(`Set: ${propertyName} => ${newVal}`);
        _val = newVal;
    };

    // 删除属性
    if (delete this[propertyName]) {
        // 创建新属性及其读取访问器、写入访问器
        Object.defineProperty(target, propertyName, {
            get: getter,
            set: setter,
            enumerable: true,
            configurable: true
        });
    }
}

class Greeter {
    @logParameter
    greeting: string;
}

const greeter = new Greeter();
greeter.greeting = 'Jerome';
greeter.greeting

运行这段代码:

$ node decorator/prototype.js 
Set: greeting => Jerome
Get: greeting => Jerome

从结果可知,在实例中使用了 getter 和 setter 方法之后,都会打印出相应的 log,即装饰器的表达式在运行的时候被调用了。

让我们来看看编译成 js 之后的 Greeter 类

var Greeter = /** @class */ (function () {
    function Greeter() {
    }
    __decorate([
        logParameter,
        __metadata("design:type", String)
    ], Greeter.prototype, "greeting", void 0);
    return Greeter;
}());

由此可见,此次 __decorate 函数传入了 4 个参数,由在类装饰器分析的 _decorate 函数可知:

​ 从右往左运行装饰器,先运行__metadata("design:type", String) 表示被装饰的参数 greeting 是 String 类型,再运行 logParameter 装饰器,改写 greeting 参数的 getter 和 setter 方法。

方法装饰器

方法装饰器声明在一个方法的声明之前(紧靠着方法声明)。 它会被应用到方法的 属性描述符上,可以用来监视,修改或者替换方法定义。

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

  • target:对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。

  • propertyKey:成员的名字。

  • descriptor:成员的属性描述符

同样地,让我们来看段代码:

export function logMethod(
    target: Object,
    propertyName: string,
    propertyDescriptor: PropertyDescriptor): PropertyDescriptor {
    const method = propertyDescriptor.value;

    propertyDescriptor.value = function (...args: any[]) {
        // 将 greet 的参数列表转换为字符串
        const params = args.map(a => JSON.stringify(a)).join();
        // 调用 greet() 并获取其返回值
        const result = method.apply(this, args);
        // 转换结尾为字符串
        const r = JSON.stringify(result);
        // 在终端显示函数调用细节
        console.log(`Call: ${propertyName}(${params}) => ${r}`);
        // 返回调用函数的结果
        return result;
    }
    return propertyDescriptor;
};

class Greeter {
    constructor(private name: string) { }

    @logMethod
    greet(message: string): string {
        return `${this.name} says: ${message}`;
    }
}

const greeter = new Greeter('Jerome');
greeter.greet('Hello');

运行这段代码:

$ node decorator/method.js 
Call: greet("Hello") => "Jerome says: Hello"

由打印结果可知,logMethod 这个方法装饰器在运行时当作了函数被调用。该函数有利于内审方法的调用,符合面向切面编程的思想。

让我们来看看编译成 js 之后的 Greeter 类:

var Greeter = /** @class */ (function () {
    function Greeter(name) {
        this.name = name;
    }
    Greeter.prototype.greet = function (message) {
        return this.name + " says: " + message;
    };
    __decorate([
        logMethod,
        __metadata("design:type", Function),
        __metadata("design:paramtypes", [String]),
        __metadata("design:returntype", String)
    ], Greeter.prototype, "greet", null);
    return Greeter;
}());

由此可见,此次 __decorate 函数传入了 4 个参数,由在类装饰器分析的 _decorate 函数可知:

​ 从右往左运行装饰器,先运行__metadata("design:returntype", String) 表示被装饰的方法 greet 的返回值的属性是 String 类型, __metadata("design:paramtypes", [String]) 表示方法参数的类型是 String,__metadata("design:type", Function) 表示被装饰的是函数类型,再运行 logMethod 装饰器,打印出了函数调用的细节。

参数装饰器

参数装饰器声明在一个参数声明之前(紧靠着参数声明)。 参数装饰器应用于类构造函数或方法声明。

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

  • target:对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。

  • propertyKey:成员的名字。

  • index:参数在函数参数列表中的索引。

同样地,让我们来看一段代码:

function logParameter(target: Object, propertyName: string, index: number) {
    // 为相应方法生成元数据键,以储存被装饰的参数的位置
    const metadataKey = `log_${propertyName}_parameters`;
    if (Array.isArray(target[metadataKey])) {
        target[metadataKey].push(index);
    }
    else {
        target[metadataKey] = [index];
    }
    console.log('target => ', target)
}

class Greeter {
    greet(@logParameter message: string, middle:string, @logParameter name: string): string {
        return `${name} : ${message}`;
    }
}
const greeter = new Greeter();
greeter.greet('hello','I am middle', 'jerome');

运行打包之后的 js:

$ node decorator/parameter.js 
target =>  Greeter { greet: [Function], log_greet_parameters: [ 2 ] }
target =>  Greeter { greet: [Function], log_greet_parameters: [ 2, 0 ] }

可以看到,打印了两个 log,message 和 name 的位置分别是 0 和 2。

让我们来看看编译成 js 之后的 Greeter 类:

var Greeter = /** @class */ (function () {
    function Greeter() {
    }
    Greeter.prototype.greet = function (message, middle, name) {
        return name + " : " + message;
    };
    __decorate([
        __param(0, logParameter), __param(2, logParameter),
        __metadata("design:type", Function),
        __metadata("design:paramtypes", [String, String, String]),
        __metadata("design:returntype", String)
    ], Greeter.prototype, "greet", null);
    return Greeter;
}());

​ 由此可见,此次 _decorate 函数也传入 4 个参数,注意第 4 个参数为 null,这在 _decorate 函数中会使 r 的值有所不同。由前面可知,装饰器从右往左运行,依次确定被装饰对象的返回值类型,参数类型以及自身的类型为 Function,这里有个不一样的 __param() 装饰器,让我们也来了解一下:

var __param = (this && this.__param) || function (paramIndex, decorator) {
    return function (target, key) {
        decorator(target, key, paramIndex);
    }
};

可见,他调用了 logParameter 函数,并传入了一个 paramIndex 表示参数的位置。由此,参数装饰器也一目了然了。

访问器装饰器

访问器装饰器应用于访问器的属性描述符,可用于观测、修改、替换访问器的定义。

function enumerable(value: boolean) {
    return function (
        target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        console.log('decorator - sets the enumeration part of the accessor');
        descriptor.enumerable = value;
    };
}

class Person {
    private _age: number = 18;
    private _name: string = 'jerome';

    @enumerable(false)
    get age() { return this._age; }

    set age(age: any) { this._age = age; }

    @enumerable(true)
    get name() {
        return this._name;
    }

    set name(name: string) {
        this._name = name;
    }

}

const person = new Person();
for (let prop in person) {
    console.log(`enumerable property = ${prop}`);
}

运行之后:

$ node decorator/defineObject.js 
decorator - sets the enumeration part of the accessor
decorator - sets the enumeration part of the accessor
enumerable property = _age
enumerable property = _name
enumerable property = name

上面的例子中,我们定义了两个访问器 name 和 age ,并通过装饰器设置是否将其列入可枚举属性,我们把 age 设置为false, 所以在清单中不会出现 age。

元数据

Reflect Metadata是 ES7 的一个提案,它主要用来在声明的时候添加和读取元数据。

可以通过 npm 安装这个库:

npm i reflect-metadata --save

TypeScript支持为带有装饰器的声明生成元数据。 你需要在命令行或 tsconfig.json 里启用 emitDecoratorMetadata 编译器选项。

命令行:

tsc --target ES5 --experimentalDecorators --emitDecoratorMetadata

tsconfig.json:

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

当启用后,只要 reflect-metadata 库被引入了,设计阶段添加的类型信息可以在运行时使用。元信息反射 API 能够用来以标准方式组织元信息。「反射」的意思是代码可以侦测同一系统中的其他代码(或其自身)。反射在组合/依赖注入、运行时类型断言、测试等使用场景下很有用。

在这里,我们能够使用之前学过的各类装饰器来修饰你的代码。

例如:

import "reflect-metadata";

// 【参数装饰器】用来存储被装饰参数的索引
export function logParameter(target: Object, propertyName: string, index: number) {
    const indices = Reflect.getMetadata(`log_${propertyName}_parameters`, target, propertyName) || [];
    indices.push(index);
    console.log('indices => ', indices);
    Reflect.defineMetadata(`log_${propertyName}_parameters`, indices, target, propertyName);
}

// 【属性装饰器】用来获取属性的运行时类型
export function logProperty(target: Object, propertyName: string): void {
    // 获取对象属性的设计类型
    var t = Reflect.getMetadata("design:type", target, propertyName);
    console.log(`${propertyName} type: ${t.name}`);
}


class Greeter {
    @logProperty
    private name: string;
    
    constructor(name: string) {
        this.name = name;
    }

    greet(@logParameter message: string): string {
        return `${this.name} says: ${message}`;
    }
}

const greeter = new Greeter('jerome');
greeter.greet('hello');

design:type 表示被装饰的对象是什么类型
design:paramtypes 表示被装饰对象的参数类型
design:returntype 表示被装饰对象的返回值属性

运行这段代码:

$ node decorator/reflect-metadata.js 
name type: String
indices =>  [ 0 ]

打印出了我们想知道的 name 的类型以及 message 的参数索引。

总结

typescript 的装饰器本质上提供了被装饰对象 Property Descriptor 的操作,都是在运行的时候被当作函数调用。看了这篇文章,是否觉得 typescript 这个装饰器的特性有点像 java spring 中注解的写法,我们可以利用我们写的装饰器来实现反射、依赖注入,类型断言等,实现在运行中程序对自身进行检查,vscode 就是典型的利用 typescript 的装饰器实现了依赖注入,有兴趣的请关注后续的文章。

扩展

装饰器是扩展 JavaScript 类的建议,TC39 五年来一直在研究装饰方案,有兴趣的可以关注下 TC39 的这个提案(https://github.com/tc39/proposal-decorators),babel 也实现了该提案,具体可查(https://babeljs.io/docs/en/babel-plugin-proposal-decorators)。

Tags