/ es7

ES6 的 装饰器 decorator

1、类的修饰 decorator(target) {}

许多面向对象的语言都有修饰器(Decorator)函数,用来修改类的行为。

@testable
class MyClass {};

function testable(target) {
    target.isTestable = true;
}

MyClass.isTestable;  // true

上面代码中,@testable就是一个修饰器。它修改了MyClass这个类的行为,为它加上了静态属性isTestable。

2、装饰器基本语法

@decorator
class A {};

// 等同于
class A {};
A = decorator(A) || A;

也就是说,修饰器是一个对类进行处理的函数。修饰器函数的第一个参数,就是所要修饰的目标类。

function decorator(target) {
    target.age = 20;   // 为类添加静态属性
    target.prototype.grade = 3;  // 为类添加实例属性
}

注意,修饰器对类的行为的改变,是代码编译时发生的,而不是在运行时。这意味着,修饰器能在编译阶段运行代码。也就是说,修饰器本质就是编译时执行的函数。

如果觉得一个参数不够用,可以在修饰器外面再封装一层函数。见下面例子:

// 定义装饰器的外层函数
function mixins(...list) {
    // 返回一个装饰器函数
    return function(target) {
        Object.assign(target.prototype, ...list);
    }
}

const args = {
    f1() {}
    f2() {}
    f3() {}
};

// 使用mixins装饰器
@mixins(args)
class MyClass {};

let inst = new MyClass();
inst.f3();

实例:实际开发中,React 与 Redux 库结合使用时,常常需要写成下面这样。

class MyComponent extends React.Component {};
export default connect(mapStateToProps, mapDispatchToProps)(MyComponent);

有了装饰器,就可以改写上面的代码。

@connect(mapStateToProps, mapDispatchToProps)
export default class MyComponent extends React.Component {};

3、修饰类的方法 decorator(target, key, descriptor) {}

修饰器不仅可以修饰类,还可以修饰类的属性。

class Person {
    @readonly
    // readonly(Person.prototype, 'name', descriptor);
    // 类似于
    // Object.defineProperty(Person.prototype, 'name', descriptor);
    name() { return `${this.name}` }
}

定义 readonly装饰器:第一个参数是类的原型对象;第二个参数是所要修饰的属性名;第三个参数是该属性的描述对象。

function readonly(target, key, descriptor) {
    // descriptor对象原来的值如下
    // {
    //     value: specifiedFunction,
    //     enumerable: false,
    //     configurable: true,
    //     writable: true
    // }
    decorator.writable = false;  // 不可写
    return descriptor;
}

再例:

class A {
    @nonenumerable
    get kidCount() {
        return this.children.length;
    }
}
function nonenumerable(target, key, descriptor) {
    descriptor.enumerable = false;
    return descriptor;
}

再例:

class Math {
    @log
    add(a, b) {
        return a + b;
    }
}
function log(target, key, descriptor) {
    var oldValue = descriptor.value;
    // 修改 descriptor属性
    descriptor.value = function() {
        console.log(`${name}`, arguments);
        return oldValue.apply(this, arguments);
    }
    return descriptor;
}

修饰器还起到了注释的作用。

用装饰器写组件:

@Component({
    tag: 'my-component',
    styleUrl: 'my-component.scss'
})
export class MyComponent {
    @Prop() first: string;
    @Prop() last: string;
    @State() isVisible: boolean = true;

    render() {
        return(
            <p>my name is {this.first} {this.last}</p>
        )
    }
}

如果同一个方法有多个修饰器,会像剥洋葱一样,先从外到内进入,然后由内向外执行。
定义一个装饰器的外层包装函数:

function dec(id) {
    // 返回一个用于修饰类成员的装饰器
    return (target, key, descriptor) => {
        descriptor.writable = false;
    }
}
class Demo {
    @dec(1)
    @dec(2)
    @dec(3)
    method() {}
}

// 装饰器的进入顺序:dec(1) -> dec(2) -> dec(3)
// 装饰器的执行顺序:dec(3) -> dec(2) -> dec(1)

除了注释,修饰器还能用来类型检查。所以,对于类来说,这项功能相当有用。从长期来看,它将是 JavaScript 代码静态分析的重要工具。

4、为什么修饰器不能用于函数?

修饰器只能用于类和类的方法,不能用于修饰函数,因为存在函数提升。
由于存在函数提升,使得修饰器不能用于函数。类是不会提升的,所以就没有这方面的问题。

如果一定要修饰函数,可以采用高阶函数的形式直接执行。

function decoratorFn(fn) {
    return function() {
        const res = fn.apply(this, arguments);
        return res;
    }
}

function doSomething() {};
const doSomething2 = decoratorFn(doSomething);

5、core-decorators.js

core-decorators.js是一个第三方模块,提供了几个常见的修饰器,通过它可以更好地理解修饰器。
(1)@autobind 修饰器使得方法中的this对象,绑定原始对象。
(2)@readonly 修饰器使得属性或方法不可写。
(3)@override 修饰器检查子类的方法,是否正确覆盖了父类的同名方法,如果不正确会报错。
(4)@deprecate 或 @deprecated 修饰器在控制台显示一条警告,表示该方法将废除。
(5)@suppressWarnings 修饰器抑制deprecated修饰器导致的console.warn()调用。但是,异步代码发出的调用除外。


import { autobind, readonly } from 'core-decorators';

class Person {
    @autobind
    getPerson() {
        return this;
    }

    @readonly
    age = 23;
}

6、使用修饰器实现自动发布事件

我们可以使用修饰器,使得对象的方法被调用时,自动发出一个事件。

使用的事件“发布/订阅”库 Postal.js

const postal = require('postal/lib/postal.lodash');

封装装饰器函数:

export default function publish(topic, channel) {
    const channelName = channel || '/';
    const msgChannel = postal.channel(channelName);
    msgChannel.subscribe(topic, v => {
        console.log('频道', channelName);
        console.log('事件', topic);
        console.log('数据', v);
    });
    // 返回装饰器函数
    return function(target, key, descriptor) {
        const fn = descriptor.value;
        descriptor.value = function() {
            let value = fn.apply(this, arguments);
            msgChannel.publish(topic, value);
        }
    };
}

使用装饰器:

import publish from './publish.js';
class FooComponent {
    @publish('foo.some.message', 'component')
    someMethod() {
        return { a: 1 };
    }
    @publish('foo.some.other')
    anotherMethod() {
        return { b: 2 };
    }
}
let foo = new FooComponent();
foo.someMethod();
foo.anotherMethod();

7、Mixin

在修饰器的基础上,可以实现Mixin模式。所谓Mixin模式,就是对象继承的一种替代方案,中文译为“混入”(mix in),意为在一个对象之中混入另外一个对象的方法。

function mix(...list) {
    return function (target) {
        Object.assign(target.prototype, ...list);
    };
}

8、Trait

Trait 也是一种修饰器,效果与 Mixin 类似,但是提供更多功能,比如防止同名方法的冲突、排除混入某些方法、为混入的方法起别名等等。

import { traits, alias } from 'traits-decorator';

class TFoo {
    foo() { console.log('foo'); }
}
const obj = {
    bar() { console.log('bar');},
    foo() { console.log('foo');}
}
@traits(TFoo, obj::alias({foo: 'aliasFoo'}))
class MyClass {};

let inst = new MyClass();
inst.foo();
inst.bar();
inst.aliasFoo();

9、 Babel 转码器 支持装饰器
目前,Babel 转码器已经支持 Decorator。

首先,安装babel-core和babel-plugin-transform-decorators。由于后者包括在babel-preset-stage-0之中,所以改为安装babel-preset-stage-0亦可。

然后,设置配置文件.babelrc。

{
    "plugins": ["transform-decorators"]
}

Babel 的官方网站提供一个在线转码器,只要勾选 Experimental,就能支持 Decorator 的在线转码。