title: ES6 - 装饰器decorator
date: 2020.12.13
top:

categories:

tags:


类的装饰
方法的装饰
为什么装饰器不能用于函数?
core-decorators.js
使用装饰器实现自动发布事件
Mixin
Trait

2020.12.13 星期日 10:40

[说明] Decorator 提案经过了大幅修改,目前还没有定案,不知道语法会不会再变。

装饰器(Decorator)是一种与类(class)相关的语法,用来注释或修改类和类方法。

装饰器是一种函数,写成@ + 函数名。它可以放在类和类方法的定义前面。

@frozen class Foo {
  @configurable(false)
  @enumerable(true)
  method() {}

  @throttle(500)
  expensiveMethod() {}
}

$_PS: 装饰者模式。babel插件:@babel/plugin-proposal-decorators

类的装饰

注意,装饰器对类的行为的改变,是代码编译时发生的,而不是在运行时装饰器本质就是编译时执行的函数。

@testable
class MyTestableClass {
  // ...
}
// ## 1.1 装饰器函数的第一个参数,就是所要装饰的目标类。
function testable(target) {
  // ...
  target.isTestable = true;
}
// ## 1.2 如果觉得一个参数不够用,可以在装饰器外面再封装一层函数。
// 装饰器testable可以接受参数,这就等于可以修改装饰器的行为。
function testable(isTestable) {
  return function(target) {
    target.isTestable = isTestable;
  }
}

@testable(true) // @testable(false)
class MyTestableClass {}
MyTestableClass.isTestable // true


// 前面的例子是为类添加一个静态属性,
// ## 2 如果想添加实例属性,可以通过目标类的prototype对象操作。
function testable(target) {
  target.prototype.isTestable = true;
}
// ### 2.1 
// mixins.js
export function mixins(...list) {
  return function (target) {
    Object.assign(target.prototype, ...list)
  }
}

// main.js
import { mixins } from './mixins'

const Foo = {
  foo() { console.log('foo') }
};

@mixins(Foo)
class MyClass {}

let obj = new MyClass();
obj.foo() // 'foo'

// 上面代码通过装饰器mixins,把Foo对象的方法添加到了MyClass的实例上面。
// ### 2.2 可以用Object.assign()模拟这个功能。
// ### 2.3 实际开发中,React 与 Redux 库结合使用时,常常需要写成下面这样。
class MyReactComponent extends React.Component {}
export default connect(mapStateToProps, mapDispatchToProps)(MyReactComponent);

// 有了装饰器,就可以改写上面的代码。
@connect(mapStateToProps, mapDispatchToProps)
export default class MyReactComponent extends React.Component {}

2 方法的装饰

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

// ## 1 装饰器函数readonly一共可以接受三个参数。
// 装饰器第一个参数是类的原型对象,
class Person {
  @readonly
  name() { return `${this.first} ${this.last}` }
}
function readonly(target, name, descriptor){
  // descriptor对象原来的值如下
  // {
  //   value: specifiedFunction,
  //   enumerable: false,
  //   configurable: true,
  //   writable: true
  // };
  descriptor.writable = false;
  return descriptor;
}

readonly(Person.prototype, 'name', descriptor);
// 类似于
Object.defineProperty(Person.prototype, 'name', descriptor);

// ## 1.2 修改属性描述对象的enumerable属性
// ## 1.3 下面的@log装饰器,可以起到输出日志的作用。

// ## 2 装饰器有注释的作用。
@testable
class Person {
  @readonly
  @nonenumerable
  name() { return `${this.first} ${this.last}` }
}
// ### 下面是使用 Decorator 写法的组件,看上去一目了然。
@Component({
  tag: 'my-component',
  styleUrl: 'my-component.scss'
})
export class MyComponent {
  @Prop() first: string;
  @Prop() last: string;
  @State() isVisible: boolean = true;

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

// 如果同一个方法有多个装饰器,会像剥洋葱一样,先从外到内进入,然后由内向外执行。

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

3 为什么装饰器不能用于函数?

装饰器只能用于类和类的方法,不能用于函数,因为存在函数提升。
另一方面,如果一定要装饰函数,可以采用高阶函数的形式直接执行。

function doSomething(name) {
  console.log('Hello, ' + name);
}

function loggingDecorator(wrapped) {
  return function() {
    console.log('Starting');
    const result = wrapped.apply(this, arguments);
    console.log('Finished');
    return result;
  }
}

const wrapped = loggingDecorator(doSomething);

4 core-decorators.js

core-decorators.js是一个第三方模块,提供了几个常见的装饰器,通过它可以更好地理解装饰器。
(1)@autobind
(2)@readonly
(3)@override
(4)@deprecate (别名@deprecated)
(5)@suppressWarnings

5 使用装饰器实现自动发布事件

它使用的事件“发布/订阅”库是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, name, descriptor) {
    const fn = descriptor.value;

    descriptor.value = function() {
      let value = fn.apply(this, arguments);
      msgChannel.publish(topic, value);
    };
  };
}

6 Mixin

在装饰器的基础上,可以实现Mixin模式。

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

不过,上面的方法会改写MyClass类的prototype对象,如果不喜欢这一点,也可以通过类的继承实现 Mixin。

let MyMixin = (superclass) => class extends superclass {
  foo() {
    console.log('foo from MyMixin');
  }
};

class MyClass extends MyMixin(MyBaseClass) {
  /* ... */
}

let c = new MyClass();
c.foo(); // "foo from MyMixin"


// ### 如果需要“混入”多个方法,就生成多个混入类。
class MyClass extends Mixin1(Mixin2(MyBaseClass)) {
  /* ... */
}

这种写法的一个好处,是可以调用super,因此可以避免在“混入”过程中覆盖父类的同名方法。

7 Trait

Trait 也是一种装饰器,效果与 Mixin 类似,但是提供更多功能,比如防止同名方法的冲突、排除混入某些方法、为混入的方法起别名等等。
下面采用traits-decorator这个第三方模块作为例子。这个模块提供的traits装饰器,不仅可以接受对象,还可以接受 ES6 类作为参数。

上面代码中,TFoo和TBar都有foo方法,结果traits装饰器报错。
一种解决方法是排除TBar的foo方法。
另一种方法是为TBar的foo方法起一个别名。
alias和excludes方法,可以结合起来使用。
as方法则为上面的代码提供了另一种写法。

11.25