资讯专栏INFORMATION COLUMN

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

xiaochao / 2280人阅读

摘要:使用装饰器的方法很简单在装饰器名前加字符,写在想要装饰的方法上,类似写注释的方式装饰器实际上是一个函数,入参为所装饰的方法,返回值为装饰后的方法。经过装饰过的方法,它依然按照原来的方式执行,只是额外执行了附件的装饰器函数的功能。

让我来深入地了解一下TypeScript对于装饰器模式的实现,以及反射与依赖注入等相关特性。

Typescript的源代码中,可以看到装饰器能用来修饰class,property,method,parameter

declare type ClassDecorator = (target: TFunction) => TFunction | void;
declare type PropertyDecorator = (target: Object, propertyKey: string | symbol) => void;
declare type MethodDecorator = (target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor) => TypedPropertyDescriptor | 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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/101503.html

相关文章

  • 装饰与元数据反射(3)参数装饰

    摘要:可见参数装饰器函数需要个参数被装饰类的原型,装饰参数所属的方法名,参数的索引。参数装饰器不应当用来修改构造器方法或属性的行为,它只应当用来产生某种元数据。一旦元数据被创建,我们便可以用其它的装饰器去读取它。 之前已经分别介绍了方法装饰器、属性装饰器和类装饰器,这篇文章我们来继续关注这些话题: 参数装饰器 装饰器工厂 我们将围绕以下这个例子,来探讨这些概念: class Person...

    Barry_Ng 评论0 收藏0
  • 装饰与元数据反射(2)属与类性装饰

    摘要:值得注意的是,的返回值复写了原始的构造函数,原因是类装饰器必须返回一个构造器函数。原始构造函数的原型被复制给的原型,以确保在创建一个的新实例时,操作符如愿以偿,具体原因可参考鄙人另一篇文章原型与对象。 上一篇文章中,我们讨论了TypeScript源码中关于方法装饰器的实现,搞明白了如下几个问题: 装饰器函数是如何被调用的? 装饰器函数参数是如何传入的? __decorate函数干了...

    Shisui 评论0 收藏0
  • 装饰与元数据反射(4)元数据反射

    摘要:庆幸的是,已经支持反射机制,来看看这个特性吧元数据反射可以通过安装包来使用元数据反射的若要使用它,我们需要在中设置为,同时添加的引用,同时加载文件。复杂类型序列化的团队为复杂类型的元数据序列化做出了努力。 本篇内容包括如下部分: 为什么JavaScript中需要反射 元数据反射API 基本类型序列化 复杂类型序列化 为什么JavaScript中需要反射? 关于反射的概念,摘自百度百...

    gaosboy 评论0 收藏0
  • 流畅的 Python - 5. 装饰闭包

    摘要:看了这一章,发现原来是装饰器,又一新知识。期间,装饰器会做一些额外的工作。书中介绍了模块中的三个装饰器。另一个是,这个装饰器把函数结果保存了起来,避免传入相同参数时重复计算。叠放不奇怪,装饰器返回的就是函数或可调用对象。 在 Web 框架 Flask 中,最常看到的或许是以@app.route开头的那行代码。由于还是刚接触 Flask,所以对这种语法还不熟悉。看了这一章,发现原来是装饰...

    Markxu 评论0 收藏0
  • Python装饰vs装饰模式

    摘要:希望引以为戒郑传装饰模式如果你了解,你肯定听过装饰器模式。在面向对象中,装饰模式指动态地给一个对象添加一些额外的职责。就增加一些功能来说,装饰模式比生成子类更为灵活。 漫谈 如果作为一个Python入门,不了解Python装饰器也没什么,但是如果作为一个中级Python开发人员,如果再不对python装饰器熟稔于心的话,那么可能并没有量变积累到质变。 我以前也看过很多讲python 装...

    stackvoid 评论0 收藏0

发表评论

0条评论

xiaochao

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<