装饰器(Decorators)提供一种简练的语法糖,这种语言特性 为 JavaScript / TypeScript 注入了新的活力。目前 Decorators 处于 Stage-2 阶段,即规范初稿阶段。

这种语言特性类似于 Python 中的 DecoratorsJavaC# 中的注解特性。

需要区别不同的是,JS中的装饰器 只能 附加在类声明,方法,访问符,属性或参数上。我们先来看一个装饰器的例子:

@frozen
class Foo {
  @configurable(false)
  @enumerable(true)
  method() {
    console.log('bar')
  }
}

通过示例可以发现,Decorators 提供了一个非常简洁的方式去声明一个变量/方法。在此例子中,我们无需了解 @frozen@configurable@enumerable 的内部实现,就可猜出它们各自的作用:如该例子中,设置类方法 method enumerable=true 和 configurable=false。

Decorators 最初是用来弥补 ECMAScript6 class 中声明的变量无法修改描述符的短板,但我们也可以利用该特性去做一些更高级的用法。

TypeScript 装饰器

DecoratorsTypeScript 中为实验性特性,必须在 tsconfig.json 或者 命令行中启用 experimentalDecorators 编译选项:

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

类装饰器

定义一个类装饰器很容易,它的第一个参数就是类构造器。

下面是一个简单的 serializable 例子说明如何定义和使用一个类装饰器:

function serializable(ctor: Function) {
    ctor.prototype.toString = function () {
        return JSON.stringify(this)
    }
}

@serializable
class User {
    // typescript shortcut
    constructor(private message: string, private age: number) {
    }
}

console.log(new User('John', 18))
// User { message: 'John', age: 18 }

方法装饰器

方法装饰器会传入三个参数:

  1. 如果是实例成员,传入类的原型 prototype,如果是静态成员,传入类的构造函数;
  2. 成员的名字;
  3. 成员的属性描述符。

以下以官方文档提供的 @enumerable 来作为示例:

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

用法:

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

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

实践

订阅事件

这个是从同事写的 Java EventBus 中获得的灵感。先上 Java 的代码:

public class UserEventSubscriber {
    @Subscribe
    public void afterLogin(UserLoginEvent event) {
        // 用户登录
    }

    @Subscribe
    public void afterLogout(UserLogoutEvent event) {
        // 用户注销
    }
}

使用 Decorators 特性,我们也可以用这个来实现相同的写法:

function subscribe(typeName: string) {
    return function (target: any, propertyKey: string) {
        event.on(typeName, target[propertyKey].bind(target));
    };
}