Javascript 装饰器极速指南


pablo.png

Decorators 是ES7中添加的JavaScript新特性。熟悉Typescript的同学应该更早的接触到这个特性,TypeScript早些时候已经支持Decorators的使用,而且提供了ES5的支持。本文会对Decorators做详细的讲解,相信你会体验到它给编程带来便利和优雅。

我在专职做前端开发之前, 是一名专业的.NET程序员,对.NET中的“特性”使用非常熟悉。在类、方法或者属性上写上一个中括号,中括号里面初始化一个特性,就会对类,方法或者属性的行为产生影响。这在AOP编程,以及ORM框架中特别有用,就像魔法一样。 但是当时JavaScript并没有这样的特性。在TypeScript中第一次使用Decorators,是因为我们要对整个应用程序的上下文信息做序列化处理,需要一种简单的方法,在原来的领域模型上打上一个标签来标识是否会序列化或者序列化的行为控制,这种场景下Decorators发挥了它的威力。 后来我们需要重构我们的状态管理,在可变的类定义和不可变对象的应用间进行转换,如果使用Decorators,不论从编的便利性还是解耦的角度都产生了令人惊喜的效果。 一直想把Decorators的相关使用整理出一个通俗的文档,使用最简单的方式来阐述这一话题,一直没有下笔。无意间在网络上发现了一篇文章(https://cabbageapps.com/fell-love-js-decorators/) , 这篇文章的行文和我要表达的内容正好相符,于是拿过来做重新编辑和改编。喜欢看英文的同学可以点击链接阅读原文。

 
giphy.gif

1.0 装饰器模式

如果我们在搜索引擎中直接搜索“decorators”或者“装饰器”,和编程相关的结果中,会看到设计模式中的装饰器模式的介绍。

 
3.png

更直观的例子如下:

 
4.png

上图中WeaponAccessory就是一个装饰器,他们添加额外的方法和熟悉到基类上。如果你看不明白没关系,跟随我一步步地实现你自己的装饰器,自然就会明白了。下面这张图,可以帮你直观的理解装饰器。

 
5.gif

我们简单的理解装饰器,可以认为它是一种包装,对对象,方法,熟悉的包装。当我们需要访问一个对象的时候,如果我们通过这个对象外围的包装去访问的话,被这个包装附加的行为就会被触发。例如 一把加了消声器的枪。消声器就是一个装饰,但是它和原来的枪成为一个整体,开枪的时候消声器就会发生作用。

从面向对象的角度很好理解这个概念。那么我们如何在JavaScript中使用装饰器呢?

1.1 开始 Decorators 之旅

Decorators 是ES7才支持的新特性,但是借助Babel 和 TypesScript,我们现在就可以使用它了, 本文以TypesScript为例。

首先修改tsconfig.json文件,设置 experimentalDecorators 和 emitDecoratorMetadata为true。

{
  "compilerOptions": {
    "target": "es2015",
    "module": "commonjs",
    "sourceMap": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true
  },
  "exclude": [
    "node_modules",
  ]
}

 

 
6.png

我们先从效果入手,然后再层层剖析。先看下面的一段代码:

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

    descriptor.value = function() {
      console.log(`Calling "${propertyKey}" with`, arguments,target);
      let value = oldValue.apply(null, [arguments[1], arguments[0]]);

      console.log(`Function is executed`);
      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"));

 

 
7.png

运行上面的代码,得到的结果如下:

 
8.png

下面我们修改代码,将第17行的注释放开。

 
9.png

再次运行代码,结果如下:

 
10.png

注意上图中左侧的输出结果,和右侧显示的代码行号。我们现在可以肯定的是,加上了 @leDecorator 标签之后,函数welcome的行为发生了改变,触发改变的地方是leDecorator函数。 根据我们上面对装饰器的基本理解,我们可以认为leDecorator是welcome的装饰器。
<b>装饰器和被装饰者之间通过 @ 符进行连接</b>。

在JavaScript层面我们已经感性的认识了装饰器,我们的代码装饰的是一个函数。在JavaScript中,一共有4类装饰器:

  • Method Decorator 函数装饰器
  • Property Decorators 熟悉装饰器
  • Class Decorator 类装饰器
  • Parameter Decorator 参数装饰器

下面我们逐一进行攻破!Come on!

 
11.jpg

1.2 函数装饰器

第一个要被攻破的装饰器是函数装饰器,这一节是本文的核心内容,我们将通过对函数装饰器的讲解来洞察JavaScript Decorators的本质。

通过使用 函数装饰器,我们可以控制函数的输入和输出。

下面是函数装饰器的定义:

MethodDecorator = <T>(target: Object, key: string, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | Void;

只要遵循上面的定义,我们就可以自定义一个函数装饰器,三个参数的含义如下:

  • target -> 被装饰的对象
  • key -> 被装饰的函数名
  • descriptor -> 被传递过来的属性的属性描述符. 可以通过 Object.getOwnPropertyDescriptor()方法来查看属性描述符。

关于属性描述符更详细内容 可以参考 https://www.jianshu.com/p/19529527df80

简单来讲,属性描述符可以用来配置一个对象的某个属性的返回值,get/set 行为,是否可以被删除,是否可以被修改,是否可以被枚举等特性。为了你能顺畅的理解装饰器,我们下面看一个直观一点的例子。

打开浏览器控制台,输入如下代码:

var o, d;
var o = { get foo() { return 17; }, bar:17, foobar:function(){return "FooBar"} };

d = Object.getOwnPropertyDescriptor(o, 'foo');
console.log(d);
d = Object.getOwnPropertyDescriptor(o, 'bar');
console.log(d);
d = Object.getOwnPropertyDescriptor(o, 'foobar');
console.log(d);

 

结果如下:

 
12.png

这里我们定义了一个对象o,定义了三个属性——foo,bar和foobar,之后通过Object.getOwnPropertyDescriptor()获取每个属性的描述符并打印出来。下面我们对value , enumerable , configurable 和 writable 做简要的说明。

  • value – >字面值或者函数/属性计算后的返回值。
  • enumerable -> 是否可以被枚举 (是否可以在 (for x in obj)循环中被枚举出来)
  • configurable – >属性是否可以被配置
  • writable -> 属性是否是可写的.

每个属性或者方法都有自己的一个描述符,通过描述符我们可以修改属性的行为或者返回值。下面关键来了:

<b>装饰器的本质就是修改描述符</b>

是时候动手写一个装饰器了。

1.2.1 方法装饰器实例

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

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

 

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

Arguments Received are World, Hello
World Hello

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

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

我们看到,方法输出值发成了变化。现在去看我们定义的方法装饰器,通过参数,leDecorator在执行时获取了调用对象的名称,被装饰方法的参数,被装饰方法的描述符。 首先通过oldValue变量保存了方法描述符的原值,即我们定义的welcome方法。接下来对descriptor.value进行了重新赋值。

 
13.png

在新的函数中首先调用了原函数,获得了返回值,然后修改了返回值。 最后return descriptor,新的descriptor会被应用到welcome方法上,此时整合函数体已经被替换了。

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

 
14.gif

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

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

现在我们完成并理解了第一个方法装饰器。下面我们来学校属性装饰器。

1.3 属性装饰器

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

属性装饰器定义如下:

PropertyDecorator = (target: Object, key: string) => void;

参数说明如下:

  • target:属性拥有者
  • key:属性名

在具体使用属性装饰器之前,我们先来简单了解下Object.defineProperty方法。Object.defineProperty方法通常用来动态给一个对象添加或者修改属性。下面是一段示例:

var o = { get foo() { return 17; }, bar:17, foobar:function(){return "FooBar"} };

Object.defineProperty(o, 'myProperty', {
get: function () {
return this['myProperty'];
},
set: function (val) {
this['myProperty'] = val;
},
enumerable:true,
configurable:true
});

 

 
16.png

在调试控制台测试上面的代码。

 
15.png

从结果中,我们看到,利用Object.defineProperty,我们动态添给对象添加了属性。下面我们基于Object.defineProperty来实现一个简单的属性装饰器。

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

 

 
17.png

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

Hi, I'm Ruban
Hi, I'm Ragul

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

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

是不是很简单呢? 接下来是Class装饰器。

1.4 Class 装饰器

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

ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction;

ClassDecorator只接收一个参数,就是Class的构造函数。下面的示例代码,修改了类原有的属性speaker,并动态添加了一个属性extra。

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

 

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

 
18.png

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

 
19.png

这里需要注意的是,<b>构造函数只会被调用一次</b>。

下面我来学习最后一种装饰器,参数装饰器。

1.5 参数装饰器

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

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

 

 
20.png

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

 
21.png

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

 
22.png

1.6 小结

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

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

下面是参考文章:
https://www.typescriptlang.org/docs/handbook/decorators.html

https://github.com/Microsoft/TypeScript-Handbook/blob/master/pages/Decorators.md

https://survivejs.com/react/appendices/understanding-decorators/

https://medium.com/google-developers/exploring-es7-decorators-76ecb65fb841

https://blog.wolksoftware.com/decorators-metadata-reflection-in-typescript-from-novice-to-expert-part-ii
https://github.com/arolson101/typescript-decorators


更多精彩内容,欢迎关注玄魂工作室微信订阅号。

 

 
24.png
posted @ 2018-04-07 23:15  玄魂  阅读(1975)  评论(0编辑  收藏  举报