site infoHacknerd | Tech Blog
blog cover

🎪 [ECMAScript6] 31. Decorator

ES6JavaScript

简介

装饰器(Decorator)用来增强 JavaScript 类(class)的功能,许多面向对象的语言都有这种语法,目前有一个提案将其引入了 ECMAScript。

装饰器是一种函数,写成@ + 函数名,可以用来装饰四种类型的值。

  • 类
  • 类的属性
  • 类的方法
  • 属性存取器(accessor)
  • javascriptCopy
    @frozen class Foo {
      @configurable(false)
      @enumerable(true)
      method() {}
    
      @throttle(500)
      expensiveMethod() {}
    }

    装饰器API

    装饰器是一个函数,API 的类型描述如下(TypeScript 写法)。

    typescriptCopy
    type Decorator = (value: Input, context: {
      kind: string;
      name: string | symbol;
      access: {
        get?(): unknown;
        set?(value: unknown): void;
      };
      private?: boolean;
      static?: boolean;
      addInitializer?(initializer: () => void): void;
    }) => Output | void;
    

    装饰器函数有两个参数。运行时,JavaScript 引擎会提供这两个参数。

  • value:所要装饰的值,某些情况下可能是undefined(装饰属性时)。
  • context:上下文信息对象。
  • 装饰器函数的返回值,是一个新版本的装饰对象,但也可以不返回任何值(void)。

    context对象有很多属性,其中kind属性表示属于哪一种装饰,其他属性的含义如下。

  • kind:字符串,表示装饰类型,可能的取值有class、method、getter、setter、field、accessor。
  • name:被装饰的值的名称: The name of the value, or in the case of private elements the description of it (e.g. the readable name).
  • access:对象,包含访问这个值的方法,即存值器和取值器。
  • static: 布尔值,该值是否为静态元素。
  • private:布尔值,该值是否为私有元素。
  • addInitializer:函数,允许用户增加初始化逻辑。
  • 装饰器的执行步骤如下。

  • 1.计算各个装饰器的值,按照从左到右,从上到下的顺序。
  • 2.调用方法装饰器。
  • 3.调用类装饰器。
  • 类的装饰器

    javascriptCopy
    @testable
    class MyTestableClass {
      // ...
    }
    
    function testable(target) {
      target.isTestable = true;
    }
    
    MyTestableClass.isTestable // true
    
    /*---- 可以多添加参数 ----*/
    
    function testable(isTestable) {
      return function(target) {
        target.isTestable = isTestable;
      }
    }
    
    @testable(true)
    class MyTestableClass {}
    MyTestableClass.isTestable // true
    
    @testable(false)
    class MyClass {}
    MyClass.isTestable // false

    方法装饰器

    方法装饰器可以返回一个新函数,取代原来的方法,也可以不返回值,表示依然使用原来的方法。如果返回其他类型的值,就会报错。下面是一个例子。

    javascriptCopy
    function replaceMethod() {
      return function () {
        return `How are you, ${this.name}?`;
      }
    }
    
    class Person {
      constructor(name) {
        this.name = name;
      }
      @replaceMethod
      hello() {
        return `Hi ${this.name}!`;
      }
    }
    
    const robin = new Person('Robin');
    
    robin.hello(), 'How are you, Robin?'

    属性装饰器

    装饰器函数readonly一共可以接受三个参数。

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

    下面是另一个例子,修改属性描述对象的enumerable属性,使得该属性不可遍历。

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

    下面的@log装饰器,可以起到输出日志的作用。

    javascriptCopy
    class Math {
      @log
      add(a, b) {
        return a + b;
      }
    }
    
    function log(target, name, descriptor) {
      var oldValue = descriptor.value;
    
      descriptor.value = function() {
        console.log(`Calling ${name} with`, arguments);
        return oldValue.apply(this, arguments);
      };
    
      return descriptor;
    }
    
    const math = new Math();
    
    // passed parameters should get logged now
    math.add(2, 4);

    装饰器不能用于函数

    装饰器只能用于类和类的方法,不能用于函数,因为存在函数提升。

    javascriptCopy
    var counter = 0;
    
    var add = function () {
      counter++;
    };
    
    @add
    function foo() {
    }

    上面的代码,意图是执行后counter等于 1,但是实际上结果是counter等于 0。因为函数提升,使得实际执行的代码是下面这样。

    javascriptCopy
    var counter;
    var add;
    
    @add
    function foo() {
    }
    
    counter = 0;
    
    add = function () {
      counter++;
    };

    addInitalizer方法

    除了属性装饰器,其他装饰器的上下文对象还包括一个addInitializer()方法,用来完成初始化操作。

    它的运行时间如下。

  • 类装饰器:在类被完全定义之后。
  • 方法装饰器:在类构造期间运行,在属性初始化之前。
  • 静态方法装饰器:在类定义期间运行,早于静态属性定义,但晚于类方法的定义。
  • 使用装饰器实现自动发布事件

    javascriptCopy
    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);
        };
      };
    }

    上面代码定义了一个名为publish的装饰器,它通过改写descriptor.value,使得原方法被调用时,会自动发出一个事件。它使用的事件“发布/订阅”库是Postal.js。

    javascriptCopy
    // index.js
    import publish from './publish';
    
    class FooComponent {
      @publish('foo.some.message', 'component')
      someMethod() {
        return { my: 'data' };
      }
      @publish('foo.some.other')
      anotherMethod() {
        // ...
      }
    }
    
    let foo = new FooComponent();
    
    foo.someMethod();
    foo.anotherMethod();

    Contents

    • 简介
    • 装饰器API
    • 类的装饰器
    • 方法装饰器
    • 属性装饰器
    • 装饰器不能用于函数
    • addInitalizer方法
    • 使用装饰器实现自动发布事件

    2024/03/24 07:04