linqinghao/blog

[译]Typescript中的装饰器与元数据反射(part1)

Opened this issue · 0 comments

这篇文章会深入讲解Typescript如何实现装饰器以及诸如反射或依赖注入这些令人激动的javascript特性。

这个系列包含了一下几个部分:
第一部分:方法装饰器
第二部分:属性装饰器 & 类装饰器
第三部分:参数装饰器 & 装饰器工厂
第四部分:类型序列化 & 元数据反射API

几个月,Microsoft 和 Google 宣布在 Typescript 和 Anaular2.0 达成合作。

我们很高兴地宣布,我们已经融合了 Typescript 和 AtScript 语言,而 Angular 2,流行的用于构建 web 站点和 web 应用程序的 JavaScript 库的下一个版本,将用 Typescript 开发。

typescript-01.jpg

注解与装饰器

这种合作促使了 Typescript 学习其他语言的特性,其中我们重点介绍注解。

注解,一种向类声明中添加元数据的方法,用于依赖注入或编译指令。

注解是由 Google AtScript 团队提出的,但是它不是正式的标准。不过,装饰器是 Yehuda Katz 提出的 ECMAScript 7 标准,用于在设计时注释和修改类和属性。

注解和装饰器几乎是相同的。

注解和 decorator 几乎是一回事。从消费者的角度来看,我们有完全相同的语法。唯一不同的是,我们无法控制如何将注解作为元数据添加到代码中。而 decorator 更像是一个构建最终成为注释的东西的接口。

但是,从长期来看,我们可以只关注 decorator,因为它们是一个真正被提议的标准。AtScript 是 Typescript,而 Typescript 实现了 decorator。

让我们先看下 Typescipt 中的装饰器。

注:如果你想了解更多注解和装饰器的区别,可以参考 Pascal Precht 这篇文章。

Typescript中的装饰器

在Typescript源码中,我们可以找到可用类型的定义。

declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;

declare type PropertyDecorator = (target: Object, propertyKey: string | symbol) => void;

declare type MethodDecorator = <T>(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void;

declare type ParameterDecorator = (target: Object, propertyKey: string | symbol, parameterIndex: number) => void;declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;

declare type PropertyDecorator = (target: Object, propertyKey: string | symbol) => void;

declare type MethodDecorator = <T>(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void;

declare type ParameterDecorator = (target: Object, propertyKey: string | symbol, parameterIndex: number) => void;

正如上面见到的,装饰器被用来对类,属性,方法,或者参数进行注解。让我们更深入的了解每一种类型的装饰器实现。

方法装饰器

现在我们已经知道了如何实现这些装饰器。首先我们先实现方法装饰器。我们新建一个方法装饰器,并且取名为log

为了实现方法装饰器,我们需要在我们希望用@字符装饰的方法前面加上装饰器的名称。正如下面的例子:

class C {
    @log
    foo(n: number) {
        return n * 2;
    }
}

在我们真正地使用@log装饰器时,我们需要先在应用的任意地方定义我们的方法装饰器。来看下log的实现。

function log(target: Function, key: string, value: any) {
    return {
        value: function (...args: any[]) {
            var a = args.map(a => JSON.stringify(a)).join();
            var result = value.value.apply(this, args);
            var r = JSON.stringify(result);
            console.log(`Call: ${key}(${a}) => ${r}`);
            return result;
        }
    };
}

一个方法装饰器有3个参数:

  • target 被装饰目标
  • key 被装饰目标的方法名
  • value 给定属性的属性描述符,如果它不存在于对象,则为undefined。通过第调用Object.getOwnPropertyDescriptor() 方法得到属性描述符。

是不是感觉很奇怪?当我们使用@log 装饰C类时,我们没有传递任何参数。因此我们应该知道是谁提供了这些参数,并且log方法在那里被调用了。我们可以在typescript编译后的代码中找到这些问题的答案。

var C = (function () {
    function C() {
    }
    C.prototype.foo = function (n) {
        return n * 2;
    };
    Object.defineProperty(C.prototype, "foo",
        __decorate([
            log
        ], C.prototype, "foo", Object.getOwnPropertyDescriptor(C.prototype, "foo")));
    return C;
})();

如果没有@log装饰器,C类生成的代码就像下面:

var C = (function () {
    function C() {
    }
    C.prototype.foo = function (n) {
        return n * 2;
    };
    return C;
})();

但是当我们添加@log装饰器时,typescript编译器向类定义添加了一下附加代码。

Object.defineProperty(
  __decorate(
    [log],                                              // decorators
    C.prototype,                                        // target
    "foo",                                              // key
    Object.getOwnPropertyDescriptor(C.prototype, "foo") // desc
  );
);

你可以阅读 MDN 文档了解更多defineProperty的用法。

object. defineproperty()方法直接在对象上定义新属性,或者修改对象上的现有属性,然后返回对象。

TypeScript 编译器传递C的原型、被修饰的方法的名称( foo )和一个名为__修饰的函数返回定义属性方法。

TypeScript 编译器使用 defineProperty 方法重写正在修饰的方法。新的方法实现将是函数__修饰返回的值。到目前为止,我们有一个新的问题: 在哪里声明的装饰功能?

如果您以前使用过 TypeScript,您可能已经知道,当我们使用 extend 关键字时,由 TypeScript 编译器生成名为 __extend 的函数。

var __extends = this.__extends || function (d, b) {
    for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
    function __() { this.constructor = d; }
    __.prototype = b.prototype;
    d.prototype = new __();
};

类似地,当我们使用 decorator 时,一个名为__decorator的函数是由TypeScript编译器生成的。让我们来看看__decorator函数。

var __decorate = this.__decorate || function (decorators, target, key, desc) {
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
    return Reflect.decorate(decorators, target, key, desc);
  }
  switch (arguments.length) {
    case 2: 
      return decorators.reduceRight(function(o, d) { 
        return (d && d(o)) || o; 
      }, target);
    case 3: 
      return decorators.reduceRight(function(o, d) { 
        return (d && d(target, key)), void 0; 
      }, void 0);
    case 4: 
      return decorators.reduceRight(function(o, d) { 
        return (d && d(target, key, o)) || o; 
      }, desc);
  }
};

上面代码片段中的第一行使用OR操作符来确保如果函数__decorator不止一次地调用,那么它就不会被一次又一次地覆盖。在第二行,我们可以观察一个条件语句:

if (typeof Reflect === "object" && typeof Reflect.decorate === "function")

这个条件语句用于检测即将发布的 JavaScript 特性: 元数据反射API(The metadata reflection API)。

注意: 我们在该系列文章的最后一篇着重讲了元数据反射api,现在我们先忽略它。

让我们回忆下我们是怎么到这里的。foo方法被函数__decorate的返回重写,该函数的调用参数如下。

__decorate(
  [log],                                              // decorators
  C.prototype,                                        // target
  "foo",                                              // key
  Object.getOwnPropertyDescriptor(C.prototype, "foo") // desc
);

我们现在了解了__decorate的实现,但因为元数据反射api无法使用,因此先回退。

// arguments.length === number fo arguments passed to __decorate()
switch (arguments.length) { 
  case 2: 
    return decorators.reduceRight(function(o, d) { 
      return (d && d(o)) || o; 
    }, target);
  case 3: 
    return decorators.reduceRight(function(o, d) { 
      return (d && d(target, key)), void 0; 
    }, void 0);
  case 4: 
    return decorators.reduceRight(function(o, d) { 
      return (d && d(target, key, o)) || o; 
    }, desc);
}

因为4个参数被传递给__decorate,因此将执行case 4。理解这段代码可能是一个挑战,因为变量的名称并不是真正的描述性的但是我们并不害怕它,对吧?

让我们从学习reduceRight方法开始。

reduceRight方法对累加器应用一个函数,数组的每个值(从右到左)都必须将其减少为一个值。

下面的代码执行完全相同的操作,但是为了便于理解,已经重写了它。

[log].reduceRight(function(log, desc) { 
  if(log) {
    return log(C.prototype, "foo", desc);
  }
  else {
    return desc;
  }
}, Object.getOwnPropertyDescriptor(C.prototype, "foo"));

当上面的代码被执行的时候,log装饰器被调用并且我们可以看到C.prototype"foo", previousValue被当作参数传递进去。所以,我们终于可以回答我们最初的问题:

  • 谁提供了这些参数?
  • log方法在哪里被调用?

如果再了解log方法的实现,我们能够更加理解当被调用时发生了些什么。

function log(target: Function, key: string, value: any) {

    // target === C.prototype
    // key === "foo"
    // value === Object.getOwnPropertyDescriptor(C.prototype, "foo")

    return {
        value: function (...args: any[]) {

            // convert list of foo arguments to string
            var a = args.map(a => JSON.stringify(a)).join();

            // invoke foo() and get its return value
            var result = value.value.apply(this, args);

            // convert result to string
            var r = JSON.stringify(result);

            // display in console the function call details
            console.log(`Call: ${key}(${a}) => ${r}`);

            // return the result of invoking foo
            return result;
        }
    };
}

结论

这是一段旅程,对吧?我希望你和我一样喜欢。我们才刚刚开始,但我们已经知道足够多了,可以创造出一些真正了不起的东西。

方法修饰符可以用于许多有趣的特性。例如,如果您曾经在SinonJS这样的测试框架中使用过spy,那么当您意识到decorator将允许我们通过添加一个@spy decorator来创建spy时,您可能会非常兴奋。

在本系列的下一章中,我们将学习如何使用属性装饰器。如果你不想错过,别忘了订阅!

如果您喜欢这篇文章,请查看 JavaScript 的结尾。在这里,我将讨论元数据注释的到来可能意味着 JavaScript 作为一种设计时编程语言的终结。

请通过@OweR_ReLoaDeD@WolkSoftwareLtd与我们讨论这篇文章