Javascript中的装饰器

装饰器草案截止到现在2019年6月26日还未进入Stage3,草案地址https://github.com/tc39/proposal-decorators 以下内容可能会在未来进入实施阶段后变更

装饰器模式大概是GOF 23中设计模式比较常见并且常用的设计模式了。ecmascrip/javascriptt语言在TC39委员会带领下也变得越来越学院化(大概不是​🤣​)

最早阅读到的相关的文章是来自于Google团队负责JS优化的Addy Osmanihttps://medium.com/google-developers/exploring-es7-decorators-76ecb65fb841,后面开始了Typescript大法,这个概念也就终于得以实践起来啦。最近在使用NestJS框架,那么这个框架的核心就是各类装饰器(浓浓的SpringBoot风啊)

Typescript实现的装饰器基本遵循了Stage2版本的草案,有以下5类装饰器

  • Class装饰器
  • Method装饰器
  • Accessor装饰器
  • Property装饰器
  • Parameter装饰器

装饰器的发挥作用肯定要处在运行期,所以对于TS来说装饰器放在声明文件或者其他可被转换成声明的文件中是没有任何作用的

具体解析执行的顺序如下

  • 对于类中的实例化成员来说Parameter  >> Method >> Accessor >> Property
  • 对于类中的静态成员来说 Parameter  >> Method >> Accessor >> Property
  • Parameter装饰器 能作用于类构造器函数
  • Class装饰器只能作用于类,并且只能影响类构造器函数

Class装饰器

它是通过影响构造器函数继而影响到类的

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

运行结果如下:

class_1 {
  property: 'property',
  hello: 'override',
  newProperty: 'new property' }

可以看到通过classDecorator作用后,类的实例增加了新的porperty,并且hello这个property的值改变成为了override

Method装饰器

method装饰器通过影响propertyDescriptor来发挥作用

propertyDescriptor是一种描述property特性的值,包含三类writable/可被改写值、configurable/可被删除和修改、enumerable/可被枚举

【以下代码需要保证target在ES5之上比如ES2015、ES2017,否则会报无法正确识别类型的错误 error TS1241: Unable to resolve signature of method decorator when called as an expression.】

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

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

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

Accessor 装饰器

同样也是通过propertyDescriptor来影响原对象,但是TS不允许同时设置某个property的set/get的装饰器,如果需要的化请使用Property装饰器,

Property装饰器

Property装饰器反而并不通过propertyDescriptor来影响原对象,因为无法在属性声明的时候来产生作用。那么只能通过Reflect-Metadata的方式来进行处理明确property name的property

@Reflect.metadata https://github.com/rbuckton/reflect-metadata/blob/master/Reflect.ts#L791

        function metadata(metadataKey: any, metadataValue: any) {
            function decorator(target: Function): void;
            function decorator(target: any, propertyKey: string | symbol): void;
            function decorator(target: any, propertyKey?: string | symbol): void {
                if (!IsObject(target)) throw new TypeError();
                if (!IsUndefined(propertyKey) && !IsPropertyKey(propertyKey)) throw new TypeError();
                OrdinaryDefineOwnMetadata(metadataKey, metadataValue, target, propertyKey);
            }
            return decorator;

Parameter装饰器

因为只用来作用于参数列表,所以并不需要返回任何值

Typescript 装饰器编译

拿官方例子来看下,TS代码

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

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

编译后代码

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;
};
var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};

let Greeter = class Greeter {
    constructor(message) {
        this.greeting = message;
    }
    greet() {
        return "Hello, " + this.greeting;
    }
};
Greeter = __decorate([
    sealed,
    __metadata("design:paramtypes", [String])
], Greeter);
function sealed(constructor) {
    Object.seal(constructor);
    Object.seal(constructor.prototype);
}

我们从编译后的代码可以看到它使用了ES6的新特性Reflect,关键就在于__decorate函数。这个函数做了如下几个事情

  • 如果支持Reflect则直接使用Reflect的decorate函数
  • 如果不支持Reflect则是直接使用倒序的方式直接执行装饰器中的函数,并将一个前一个函数的执行结果作为下一个函数的参数传入