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

Great! You've successfully subscribed.
Great! Next, complete checkout for full access.
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.