装饰器

Decorators 是ES7中添加的JavaScript新特性。

在JavaScript中,一共有4类装饰器:

  1. Method Decorator 函数装饰器
  2. Property Decorators 熟悉装饰器
  3. Class Decorator 类装饰器
  4. Parameter Decorator 参数装饰器

方法装饰器实例

下面我们通过方法装饰器来修改一个函数的输入和输出。

function leDecorator(target, propertyKey: string, descriptor: PropertyDescriptor): any {
    var oldValue = descriptor.value;

    descriptor.value = function() {
      console.log(`Calling "${propertyKey}" with`, arguments,target);
      // Executing the original function interchanging the arguments
      let value = oldValue.apply(null, [arguments[1], arguments[0]]);
      //returning a modified value
      return value + "; This is awesome";
    };

    return descriptor;
  }

  class JSMeetup {
    speaker = "Ruban";
    //@leDecorator
    welcome(arg1, arg2) {
      console.log(`Arguments Received are ${arg1}, ${arg2}`);
      return `${arg1} ${arg2}`;
    }
  }

  const meetup = new JSMeetup();

  console.log(meetup.welcome("World", "Hello"));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

在不使用装饰器的时候,输出值为:

Arguments Received are World, Hello
World Hello
1
2

启用装饰器后,输出值为:

Calling "welcome" with { '0': 'World', '1': 'Hello' } JSMeetup {}
Arguments Received are Hello, World
Hello World; This is awesome
1
2
3

通过使用装饰器,我们实现了对原函数的包装,可以修改方法的输入和输出,这意味着我们可以应用各种想要的魔法效果到目标方法上。

这里有几点需要注意的地方:

  • 装饰器在class被声明的时候被执行,而不是class实例化的时候。
  • 方法装饰器返回一个值
  • 存储原有的描述符并且返回一个新的描述符是我们推荐的做法. 这在多描述符应用的场景下非常有用。
  • 设置描述符的value的时候,不要使用箭头函数。

属性装饰器

属性装饰器和方法装饰器很类似,通过属性装饰器,我们可以用来重新定义getters、setters,修改enumerable, configurable等属性

function realName(target, key: string): any {
    // property value
    var _val = target[key];

    // property getter
    var getter = function () {
      return "Ragularuban(" + _val + ")";
    };

    // property setter
    var setter = function (newVal) {
      _val = newVal;
    };

    // Create new property with getter and setter
    Object.defineProperty(target, key, {
      get: getter,
      set: setter
    });
  }

  class JSMeetup {
    //@realName
    public myName = "Ruban";
    constructor() {
    }
    greet() {
      return "Hi, I'm " + this.myName;
    }
  }

  const meetup = new JSMeetup();
  console.log(meetup.greet());
  meetup.myName = "Ragul";
  console.log(meetup.greet());
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

在不适用装饰器时,输出结果为:

Hi, I'm Ruban
Hi, I'm Ragul
1
2

启用装饰器之后,结果为:

Hi, I'm Ragularuban(Ruban)
Hi, I'm Ragularuban(Ragul)
1
2

Class 装饰器

Class装饰器是通过操作Class的构造函数,来实现对Class的相关属性和方法的动态添加和修改。

function AwesomeMeetup<T extends { new (...args: any[]): {} }>(constructor: T) {
    return class extends constructor implements extra {
      speaker: string = "Ragularuban";
      extra = "Tadah!";
    }
  }

  //@AwesomeMeetup
  class JSMeetup {
    public speaker = "Ruban";
    constructor() {
    }
    greet() {
      return "Hi, I'm " + this.speaker;
    }
  }

  interface extra {
    extra: string;
  }

  const meetup = new JSMeetup() as JSMeetup & extra;
  console.log(meetup.greet());
  console.log(meetup.extra);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

在不启用装饰器的情况下输出值为:

Hi, I'm Ruban
undefined
1
2

在启用装饰器的情况下,输出结果为:

Hi, I'm Ragularuban
Tadah!
1
2

参数装饰器

如果通过上面讲过的装饰器来推论参数装饰器的作用,可能会是修改参数,但事实上并非如此。参数装饰器往往用来对特殊的参数进行标记,然后在方法装饰器中读取对应的标记,执行进一步的操作。例如:

function logParameter(target: any, key: string, index: number) {
    var metadataKey = `myMetaData`;
    if (Array.isArray(target[metadataKey])) {
      target[metadataKey].push(index);
    }
    else {
      target[metadataKey] = [index];
    }
  }

  function logMethod(target, key: string, descriptor: any): any {
    var originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {

      var metadataKey = `myMetaData`;
      var indices = target[metadataKey];
      console.log('indices', indices);
      for (var i = 0; i < args.length; i++) {

        if (indices.indexOf(i) !== -1) {
          console.log("Found a marked parameter at index" + i);
          args[i] = "Abrakadabra";
        }
      }
      var result = originalMethod.apply(this, args);
      return result;

    }
    return descriptor;
  }

  class JSMeetup {
    //@logMethod
    public saySomething(something: string, @logParameter somethingElse: string): string {
      return something + " : " + somethingElse;
    }
  }

  let meetup = new JSMeetup();

  console.log(meetup.saySomething("something", "Something Else"));

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

上面的代码中,我们定义了一个参数装饰器,该装饰器将被装饰的参数放到一个指定的数组中。在方法装饰器中,查找被标记的参数,做进一步的处理 不启用装饰器的情况下,输出结果如下:

something:Something Else
1

启用装饰器的情况下,输出结果如下:

Found a marked parameter at index 1
something:Abrakadabra
1
2

小结

现在我们已经学习了所有装饰器的使用,下面总结一下关键用法:

  1. 方法装饰器的核心是 方法描述符
  2. 属性装饰器的核心是 Object.defineProperty
  3. Class装饰器的核心是 构造函数
  4. 参数装饰器的主要作用是标记,要结合方法装饰器来使用

参考文档:

https://juejin.im/post/5ac85f1d6fb9a028bf0590ee

https://juejin.im/post/5b6156e4e51d45355d51f819

https://juejin.im/post/5b41f76be51d4518f140f9e4