Javascript 装饰器极速指南


本文主要参考自 https://cabbageapps.com/fell-love-js-decorators/。 并未按原文严格翻译,做了重新整理和编辑,部分内容做了大范围修改。

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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容