1

浅析 TypeScript 装饰器

 2 years ago
source link: https://jelly.jd.com/article/61558dbab5b99e018e88e26b
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
浅析 TypeScript 装饰器
上传日期:2021.10.25
当我们在进行一个较大的项目开发时,往往会使用到不同的类对象,以及类对象之间的继承,而当我们在不同的类中有与函数本身逻辑无关的或需要复用共享的代码时,代码的可读性与复用性会被削弱很多(俗称不够优雅),为了应对这种情况,TypeScript 引入了一项实验性特性,它就是装饰器(Decorators)。

一、装饰器的介绍

1.1 什么是装饰器

装饰器本质上是一个独立的函数,它可以对类、方法、访问器、属性或参数进行修饰,且一个装饰器可以装饰多个对象,去掉装饰器后被装饰对象依然可以正常的使用。

举个栗子:某大户人家,家里有两个女儿,每个女儿都有一个洋娃娃,二女儿看到姐姐有一根发带很漂亮,就借过来戴一了一天,作为交换,二女儿把自己洋娃娃最漂亮的一套衣服借给了姐姐,姐姐看着穿上漂亮衣服的洋娃娃也开心了一整天。

在这个例子里,如果把某大户人家看作是某项目,那么家里的两个女儿就是项目下的两个类,女儿的洋娃娃则为类下面的方法,那么发带就是类的装饰器,洋娃娃的衣服就是方法的装饰器。女儿之间可以共享发带,某个女儿没有戴发带也可以过得很开心。

1.2 什么场景下需要用到装饰器

当我们需要对类或其成员进行标注或修改、对方法进行修改如添加防抖或节流、对访问器的属性描述符进行修改、对属性进行过滤或排序、对参数进行类型判断或校验等操作,而操作内容需要被多处用到,或其与被操作的对象本身逻辑无关时,就可以使用装饰器增强代码的复用性与可读性。

既然装饰器可以让代码更加“优雅”,那我们该怎样去使用它呢?

二、装饰器的使用

2.1 环境配置

因为在 TypeScript 中装饰器还是一项实验性特性,因此默认是关闭的,需要启用的话需要在命令行或 tsconfig.json 里启用 experimentalDecorators 编译器选项:

  tsc --target ES5 --experimentalDecorators

tsconfig.json:

  {
    "compilerOptions": {
      "target": "ES5",
      "experimentalDecorators": true
    }
  } 

环境配置好后,就是可以正式开始使用装饰器了。

2.2 如何声明

装饰器本质上是一个函数,根据使用场景的不同,被装饰的声明信息会被做为一个或多个特定的参数传入:

  function log(target: function): void { // 这是装饰器
    console.log('Hello World')
  }

当需要向装饰器传入自定义的参数时,需要使用工厂函数接收参数,然后在工厂函数内部将装饰器函数返回出去:

  function logWithString(args: string) { // 这是装饰器工厂函数
    return function (target: function): void { // 这是装饰器
      console.log(args)
    }
  }

2.3 如何使用

装饰器使用 @expression 的形式,expression 求值后必须为一个函数,它会在运行时被调用,使用的时候需要添加在被装饰对象的前面。

  @log // 或 @logWithString('Hello World')
  class A {}

如上述代码,使用 2.2.1 中的两个例子编译后,执行的结果均为“Hello World”。

当然,也可以使用多个装饰器装饰同一个对象,它们可以书写在同一行或者多行上:

  @logWithString('Hello World')  @log  class A {}
  @logWithString('Hello World')
  @log
  class A {

  }

其从右到左、从下到上依次执行,执行结果当于

  logWithString(log(function A() {}))

最后打印的结果是两个“Hello World”。

三、装饰器的类型

装饰器的类型分为:类装饰器、方法装饰器、访问器装饰器、属性装饰器,还有参数装饰器。

3.1 类装饰器

类装饰器紧靠着类来声明,类装饰器应用于类构造函数,可以用来监视、修改或替换类定义。

类的构造函数是类装饰器的唯一的参数。

  function changeUser(args: string) {
    return function (target: function): void {
      target.prototype.user = args
    }
  }

  @changeUser('Peter')
  class Welcome {
    user: string

    showWarn ():void {
      console.log('Hello, ' + this.user)
    }
  }

  const welcome: Welcome = new Welcome()
  welcome.showWarn() // Hello, Peter

3.2 方法装饰器

方法装饰器紧靠着一个方法来声明,它会被应用到方法的属性描述符上,可以用来监视、修改或者替换方法定义。

方法装饰器接收3个参数:

  1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  2. 成员的名字。
  3. 成员的属性描述符。
  function writable(value: boolean) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        console.log(target)
        console.log(propertyKey)
        console.log(descriptor)

        descriptor.writable = value
    }
  }

  class Welcome {
    user: string
    constructor(user: string) {
      this.user = user
    }

    @writable(false)
    showWarn() {
      return "Hello, " + this.user
    }
  }

  const welcome = new Welcome('Peter')

  welcome.showWarn = () => {
    return '1'
  }

  console.log(welcome.showWarn())

输入结果如下:

  { showWarn: [Function (anonymous)] }
  showWarn
  {
    value: [Function (anonymous)],
    writable: true,
    enumerable: true,
    configurable: true
  }
  Hello, Peter

方法装饰器中成员的属性描述符参数可以用来设置成员的configurable、enumerable、writable等属性,就像上面的例子,使用方法修饰器将 showWarn 方法的 writable 属性描述符设置为 false 之后,Welcome 类的实例化对象的 showWarn 方法不能被修改。

因此,方法装饰器的 descriptor 的有点类似 Object.defineProperty() 的 descriptor。

3.3 访问器装饰器

访问器装饰器紧靠着访问器来声明,它应用于访问器的属性描述符并且可以用来监视、修改或替换一个访问器的定义。

注意  TypeScript 不允许同时装饰一个成员的 get 和 set 访问器。因此,如果想为一个成员的访问器添加装饰器,则必须添加在该成员在文档顺序上的第一个访问器前。因为装饰器应用于属性描述符时联合了 get 和 set 访问器,而不是分开声明的。

访问器装饰器接收3个参数:

  1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  2. 成员的名字。
  3. 成员的属性描述符。
  function configurable(value: boolean) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
      descriptor.configurable = value
      console.log(descriptor)
    }
  }

  class Welcome {
    user: String
    constructor(user: String) {
      this.user = user
    }

    @configurable(false)
    set setUser(user: String) {
      this.user = user
    }

    get getUser() {
      return 'Hello, ' + this.user
    }
  }

  const welcome = new Welcome('Peter')
  console.log(welcome.getUser)

输出结果如下:

  {
    get: undefined,
    set: [Function: set],
    enumerable: false,
    configurable: false
  }
  Hello, Peter

访问器装饰器和方法装饰器的入参几乎一致,但是对比 3.2 方法装饰器中例子的输出结果可以看出:访问器装饰器的 descriptor 有 get 和 set 属性,但没有方法装饰器中的 value 和 witable 属性。

3.4 属性装饰器

属性装饰器紧靠着属性来声明,它可以用来监视、修改或替换一个属性的定义。

属性装饰器接收2个参数:

  1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  2. 成员的名字。
  function newUser(target: any, propertyKey: string) {
    let value = target[propertyKey]

    Object.defineProperty(target, propertyKey, {
      set: function (newVal) {
        value = newVal
      },
      get: function () {
        return 'Hello, ' + value
      },
      enumerable: true,
      configurable: true
    })
  }

  class Welcome {
    @newUser
    user: String
    constructor(user: String) {
      this.user = user
    }
  }

  const welcome = new Welcome('Peter')
  console.log(welcome.user) // Hello, Peter

3.5 参数装饰器

参数装饰器紧靠着参数来声明,它接收3个参数:

  1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  2. 成员的名字。
  3. 参数在函数参数列表中的索引。
  function checkIn(target: any, propertyKey: string, parameterIndex: number) {
    console.log(target, propertyKey, parameterIndex)
    console.log(`一共有${parameterIndex + 1}位客人`)
    target.fullNumber = parameterIndex + 1
  }

  class Welcome {
    users: string[]
    comeNumber: number
    fullNumber: number 

    come( user?: String, user1?: String, @checkIn user2?: String) {
      let array = []
      for(let i = 0; i < arguments.length; i++){
        array.push(arguments[i])
      }
      this.comeNumber = array.length
      this.users = array
    }

    showWelcome () {
      if (this.comeNumber === this.fullNumber ) {
        console.log(`客人来齐了,Hello, ${this.users.join(', ')}`)
      } else {
        console.log(`客人还差${this.fullNumber - this.comeNumber}位,请稍等`)
      }
    }
  }

  const welcome = new Welcome()
  welcome.come('Peter', 'Lucy')
  welcome.showWelcome()
  welcome.come('Peter', 'Lucy', 'Rose')
  welcome.showWelcome()

输出结果如下:

  { come: [Function (anonymous)], showWelcome: [Function (anonymous)] } come 2
  一共有3位客人
  客人还差1位,请稍等
  客人来齐了,Hello, Peter, Lucy, Rose

四、装饰器的原理

装饰器的类型有这么多种,那它具体是怎么实现的呢?

编译上文中 ”3.2 方法装饰器“ 的例子并格式化文件后,生成了如下的js文件:

  var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length,
      r =
        c < 3
          ? target
          : desc === null
          ? (desc = Object.getOwnPropertyDescriptor(target, key))
          : desc,
      d;

    if (typeof Reflect === "object" && typeof Reflect.decorate === "function")
      r = Reflect.decorate(decorators, target, key, desc);
    else
      for (var i = decorators.length - 1; i >= 0; i--)
        if ((d = decorators[i]))
          r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
  };

  function writable(value) {
    return function (target, propertyKey, descriptor) {
      console.log(target);
      console.log(propertyKey);
      console.log(descriptor);
      descriptor.writable = value;
    };
  }

  var Welcome = /** @class */ (function () {
    function Welcome(user) {
      this.user = user;
    }
    Welcome.prototype.showWarn = function () {
      return "Hello, " + this.user;
    };
    __decorate([writable(false)], Welcome.prototype, "showWarn", null);
    return Welcome;
  })();

  var welcome = new Welcome("Peter");
  welcome.showWarn = function () {
    return "1";
  };
  console.log(welcome.showWarn());

可以看到编译出来的 Welcome 构造函数在 return 之前执行了一个 __decorate 方法,它接收了四个参数,第一个参数是调用的装饰器方法组成的数组,第二个是构造函数的原型,第三个是装饰器关联的方法,第四个是 null。

让我们看一下 __decorate 这个方法。

  var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {}

它的第一行是用来避免被重复定义的,可见即使同时使用了多个不同类型的装饰器,它们在编译后执行的都是同一个 __decorate 方法。

  var c = arguments.length,
    r =
      c < 3
        ? target
        : desc === null
        ? (desc = Object.getOwnPropertyDescriptor(target, key))
        : desc,
    d;

然后再看第二行,定义了一个 c 为入参数量,定义了一个 r 由入参来决定它是 desc 或 target.key 的自身属性描述符或 target,最后声明了一个 d。

  if (typeof Reflect === "object" && typeof Reflect.decorate === "function")
    r = Reflect.decorate(decorators, target, key, desc);

第三行为检测是否安装有 reflect-metadata 库来支持实验性的 metadata API ,使用它可以使 TypeScript 支持为带有装饰器的声明生成元数据,由于暂时没有使用到元数据,我们可以继续来看 else 语句执行的内容。

   for (var i = decorators.length - 1; i >= 0; i--)
        if ((d = decorators[i]))
          r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;

在这里循环了 decorators 参数,并将它赋值给了前面声明的 d 变量,当 d 存在的时候则对 r 进行重新赋值,从下面执行的 __decorate 可以看出 c 为 4,则 r 为 d(target, key, r),即通过 d 方法处理过的 target.key 的自身属性描述符

  return c > 3 && r && Object.defineProperty(target, key, r), r;

最后如果 c > 3 且存在 r ,则修改 target.key 的属性为 r,并将 r 返回出去。

现在结合定义的 writable 装饰器就可以看出对应执行的内容为:

  function (target, propertyKey, descriptor) {
    console.log(target); // Welcome.prototype
    console.log(propertyKey); // "showWarn"
    console.log(descriptor); // Object.getOwnPropertyDescriptor(Welcome.prototype, "showWarn")
    descriptor.writable = value;
  }

即最后执行的结果为:

  let descriptor = Object.getOwnPropertyDescriptor(Welcome.prototype, "showWarn")
  descriptor.writable = false
  Object.defineProperty(Welcome.prototype, "showWarn", descriptor)

装饰器使得 TypeScript 拥有了和 Python 和 Java 等语言类似的扩展功能,希望它可以早日转正,帮助我们日渐臃肿的身躯和代码一起走向曼妙和优雅。

六、参考文献

TypeScript 装饰器


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK