Decorator

March 24, 2021

前言

ESnext 里面提到的修饰器,在 redux 的推广下,几乎每个工程师都有或多或少的用过,最常见的就是 @connect 的形式,而在 Java 领域同样也存在 @ 这种符号的存在,被称之为注解,而巧的是修饰器同样能实现注解的功能。在看 Java 的注解的时候必然会看到反射这个概念。在阅读下面之前请先看阮老师的文档修饰器,再看下文。

Babel 开发环境须知

修饰器 Decorator 是 ES7 里面提出的,在 babel 6 里面需要引入 preset-stage-2,并在 .babelrc 中配置 "presets": ["env", "stage-2"]。到了 babel 7.0.0-beta.54 之前,则是需要 npm 包 @babel/preset-stage-2,配置 .babelrc 为 ["@babel/preset-stage-2", { "decoratorsLegacy": true }],默认是关闭的。而 babel 7.0.0-beta.54 之后的版本里面,已经 弃用 Stage Preset ,所以后面需要安装的版本是 @babel/plugin-proposal-decorators 配置为 ["@babel/plugin-proposal-decorators", { "legacy": true }],这样可以达到以前的效果,具体看官方介绍,以及@babel/plugin-proposal-decorators。目前 babel 7.1.0 已经发布了TC39 Standards Track Decorators in Babel

java 中的注解和反射

在 Java 里面注解是元数据,相当于是提供元素的配置信息,也就是额外的数据,比如最常见的 @Override @RequestMapping 这些。而在 ES 里面修饰器则更强大的多,是对类进行处理封装的改变,也可以不使用 @ 符号进行描述,但是这样就失去其便捷性、直观性了。ESnext 中关于 Decorator 的提案中提到修饰器不仅仅是可以用来修饰类,更包括了字段、getter、setter 和 方法。

先来看看 Java 中的注解是什么用法:

public class MyClass {
  ...
  @Test public void getName()
}

通过 Test 注解,来添加额外的信息,本身是没有什么作用的,需要工具支持才可以用,而这个工具就是反射了。反射有什么作用呢?反射可以在运行时获得程序或程序集中每一个类型的成员和成员的信息 。如果结合上注解,那就是 可以在运行时通过反射的方式取出方法的注解 ,从而实现额外的功能。反射里面主要用到下面四个类:

  1. java.lang.Class 类对象;
  2. java.lang.reflect.Constructor 构造器对象 有 public Constructor[] getConstructors() 方式;
  3. java.lang.reflect.Method 方法对象,有 public Method[] getMethods() 等;
  4. java.lang.reflect.Field 属性对象,有 public Field[] getFields() 等;

可以看看下面示例了解一下反射:

for (Method m : obj.getClass().getMethods()) {
  Test t = m.getAnnotation(Test.class);
  if (t != null) {
    // 对符合要求的 m 做处理
  }
}

反射的存在使得注解变得灵动起来。注解除了是 JDK 或者 Spring 里面提供的外,还可以自己定义注解,形如下面的 @Override 注解:

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

ES 中的修饰器

ES 中对类的修饰器,更像是给类加个包装,对这个类进行操作。这里可以看看 react-redux 里面的 connect 的实现:

function connectAdvanced() {
  return function wrapWithConnect(WrappedComponent) {
    // ...
    class Connect extends Component {
      // ...
      render() {
        if (this.state.error) {
          throw this.state.error
        } else {
          return createElement(WrappedComponent, this.addExtraProps(this.state.props))
        }  
      }
    }
    // ...
    return hoistStatics(Connect, WrappedComponent)
  }
}

// 使用方式
@connect(state => ({
  global: state.global
}))
class WrappedComponent extends PureComponent {}

如上所示的,通过 @connect,对 WrappedComponent 修饰,在 Connect 中实现一套更新 store 的逻辑,最后通过 setState 来触发,使得传入 WrappedComponent 的 props 发生更新,并注入 dispath 等等方法,最后达到传递状态的作用。 而上面的这一切的功能只要用一个 @connect 就可以办到,和 Java 的注解不同,这里不需要反射来实现其功能。

对类/方法修饰还可以看看方正大神的(egg-blueprint)[https://github.com/Foveluy/egg-blueprint/blob/master/lib/index.js],是一个 egg.js 为插件,采用修饰器的方式,实现了类似于 Spring 中 @RequestMapping 的方式。

对于一些 java 中常见的注解,如 @override @readonly 等等,在 ES 里面也有同样的实现!比如常用的 core-decoratorslodash-decorators 库。

比如 @readonly 的实现:

function readonly(target, name, descriptor){
  descriptor.writable = false;
  return descriptor;
}

如果说对类的修饰是外在包装一层,那么对方法的修饰,由于 descriptor 的存在,则更像是修饰器。方法的修饰里面传入的参数中,包含了 descriptor 描述对象。这个在 Object.defineProperty 中应该是再熟悉不过的了。通过 descriptor 可以实现非常多的功能。

对于类的属性同样也能采用修饰器的方式,类似于方法,但是其 descriptor 属性描述符里面只能用 initializer 来替代之前的 value。

另外 ESnext 里面也可以通过 # 来定义私有变量,就如同 java 的修饰符 private。

只是对于私有方法和私有变量而言,其 descriptor 属性描述符里面 writable enumerable configurable 均为 false,同样的私有变量的描述对象只能用 initializer 来替代的 value。其他静态方法/变量等可以看看文档

总结

由于对 java 的理解尚浅,本文也是做个简单的对比。ES 中的修饰器随着新特性的出现,也会被更多人所使用。