[译]Typescript中的装饰器与元数据反射(part1)
Opened this issue · 0 comments
这篇文章会深入讲解Typescript如何实现装饰器以及诸如反射或依赖注入这些令人激动的javascript特性。
这个系列包含了一下几个部分:
第一部分:方法装饰器
第二部分:属性装饰器 & 类装饰器
第三部分:参数装饰器 & 装饰器工厂
第四部分:类型序列化 & 元数据反射API
几个月,Microsoft 和 Google 宣布在 Typescript 和 Anaular2.0 达成合作。
我们很高兴地宣布,我们已经融合了 Typescript 和 AtScript 语言,而 Angular 2,流行的用于构建 web 站点和 web 应用程序的 JavaScript 库的下一个版本,将用 Typescript 开发。
注解与装饰器
这种合作促使了 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与我们讨论这篇文章