装饰器与元数据反射(1)方法装饰器

3次阅读

共计 3573 个字符,预计需要花费 9 分钟才能阅读完成。

让我来深入地了解一下 TypeScript 对于装饰器模式的实现,以及反射与依赖注入等相关特性。
在 Typescript 的源代码中,可以看到装饰器能用来修饰 class,property,method,parameter:
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;
}
}
装饰器实际上是一个函数,入参为所装饰的方法,返回值为装饰后的方法。在使用之前需要提前实现这个装饰器函数,如下:
function log(target: Function, key: string, descriptor: any) {
// target === C.prototype
// key === “foo”
// descriptor === Object.getOwnPropertyDescriptor(C.prototype, “foo”)
// 保存对原方法的引用,避免重写
var originalMethod = descriptor.value;

descriptor.value = function (…args: any[]) {
// 将“foo”函数的参数列表转化为字符串
var a = args.map(a => JSON.stringify(a)).join();
// 调用 foo() 并获取它的返回值
var result = originalMethod.apply(this, args);
// 将返回的结果转成字符串
var r = JSON.stringify(result);
// 打印日志
console.log(`Call: ${key}(${a}) => ${r}`);
// 返回调用 foo 的结果
return result;
}

// 返回已编辑的描述符
return descriptor;
}
该装饰器函数包含三个参数:

target:所要修饰的方法。

key:被修饰方法的名字。

descriptor:属性描述符,如果为给定可以通过调用 Object.getOwnPropertyDescriptor() 来获取。

我们观察到,类 C 中使用的装饰器函数 log 并没有显式的参数传递,不免好奇它所需要的参数是如何传递的?以及该函数是如何被调用的?
TypeScript 最终还是会被编译为 JavaScript 执行,为了搞清上面的问题,我们来看一下 TypeScript 编译器将类 C 的定义最终生成的 JavaScript 代码:
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;
})();
而为添加装饰器所生成的 JavaScript 代码如下:
var C = (function () {
function C() {
}
C.prototype.foo = function (n) {
return n * 2;
};
return C;
})();
对比两者发现使用装饰的不同,只是在类定义中,多了如下代码:
Object.defineProperty(
__decorate(
[log], // 装饰器
C.prototype, // target:C 的原型
“foo”, // key:装饰器修饰的方法名
Object.getOwnPropertyDescriptor(C.prototype, “foo”) // descriptor
);
);
通过查询 MDN 文档,可以知悉 defineProperty 的作用:

Object.defineProperty() 方法可直接在一个对象上定义一个新的属性,或者修改对象上一个已有的属性,然后返回这个对象。
TypeScript 编译器通过 defineProperty 方法重写了所修饰的方法 foo,新方法的实现是由函数__decorate 返回的,那么问题来了:__decorate 函数在哪声明的呢?
掘地三尺不难找到,来一起把玩一下:
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);
}
};
第一行使用了或操作符(||),以确保如果函数__decorate 已被创建,他将不会被重写。
if (typeof Reflect === “object” && typeof Reflect.decorate === “function”)
第二行是一个条件语句,使用了 JavaScript 的一个新特性:元数据反射。这个主题后续再展开讲述,下面我们先聚焦观察下该新特性的兼容方案:
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);
}
此处__decorate 函数接受了 4 个参数,所以 case 4 将被执行。平心而论这块代码有点生涩,没关系掰开揉碎了看。

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 也被传入,如此之前的问题现在可以解答了。经过装饰过的 foo 方法,它依然按照原来的方式执行,只是额外执行了附件的装饰器函数 log 的功能。
const c = new C();
const r = c.foo(23); // “Call: foo(23) => 46”
console.log(r); // 46

正文完
 0