JavaScript 装饰器极速指南


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

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

1.0 装饰器模式

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

更直观的例子以下:java

上图中WeaponAccessory就是一个装饰器,他们添加额外的方法和熟悉到基类上。若是你看不明白不要紧,跟随我一步步地实现你本身的装饰器,天然就会明白了。下面这张图,能够帮你直观的理解装饰器。node

5.gif

咱们简单的理解装饰器,能够认为它是一种包装,对对象,方法,熟悉的包装。当咱们须要访问一个对象的时候,若是咱们经过这个对象外围的包装去访问的话,被这个包装附加的行为就会被触发。例如 一把加了消声器的枪。消声器就是一个装饰,可是它和原来的枪成为一个总体,开枪的时候消声器就会发生做用。react

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

1.1 开始 Decorators 之旅

Decorators 是ES7才支持的新特性,可是借助Babel 和 TypesScript,咱们如今就可使用它了, 本文以TypesScript为例。程序员

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

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

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

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"));
复制代码

运行上面的代码,获得的结果以下:

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

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

注意上图中左侧的输出结果,和右侧显示的代码行号。咱们如今能够确定的是,加上了 @leDecorator 标签以后,函数welcome的行为发生了改变,触发改变的地方是leDecorator函数。 根据咱们上面对装饰器的基本理解,咱们能够认为leDecorator是welcome的装饰器。 装饰器和被装饰者之间经过 @ 符进行链接

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

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

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

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);
复制代码

结果以下:

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

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

每一个属性或者方法都有本身的一个描述符,经过描述符咱们能够修改属性的行为或者返回值。下面关键来了:

装饰器的本质就是修改描述符

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

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进行了从新赋值。

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

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

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

  • 装饰器在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
});
复制代码

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

从结果中,咱们看到,利用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());
复制代码

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

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);
复制代码

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

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

这里须要注意的是,构造函数只会被调用一次

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

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

复制代码

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

启用装饰器的状况下,输出结果以下:

1.6 小结

如今咱们已经学习了全部装饰器的使用,下面总结一下关键用法:

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

更多前端好文,关注微信订阅号“玄魂工做室”,回复“qd” 便可

玄魂工做室
下面是参考文章: 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

相关文章
相关标签/搜索