> 文档中心 > TypeScript笔记(15)—— 深入理解TypeScript中的装饰器

TypeScript笔记(15)—— 深入理解TypeScript中的装饰器

TypeScript笔记(15):TypeScript装饰器


CSDN:jcLee95
邮箱:291148484@163.com

专题目录:https://blog.csdn.net/qq_28550263/article/details/115601920

如果你对TypeScript装饰器有所疑惑,那么欢迎阅读本文。希望对你有所帮助,感谢支持!


【导读】:和其它很多功能一样,装饰器也不是TypeScript独有。实际上装饰器是一种用于拓展、修改对象行为的一种软件涉及模式。这种模式提供了我们不用知道原对象就可以对其进行对其更改的特点。

1. 类 装饰器

  • 用于对类进行声明:本质上是对类的构造进行包装(修改)。我们 可以为同一个类定义多个装饰器,通过@decorator_name的形式写在类声明之前。
  • 参数:类的构造函数作为其唯一的参数。

定义类装饰器的基本格式为:

function classDec1(constructor) {}

使用类装饰器的格式为将@装饰器名紧写在被装饰的类上,后面其它装饰器多数类似(除参数装饰器写在形参前)。即装饰器一般这样应用:

@decorator_nameclass{  ...}

示例1. 多个类装饰器装饰于同一个类的执行顺序:

function classDec1(constructor: Function) {   /    * 类装饰器1:对类装饰类的constructor做一些事情    * @param {constructor}:Function被装饰类的构造函数作为其唯一参数    */   console.log('执行类装饰器`classDec1`的语句');   console.log('类装饰器语句执行:可在构造类前对类构造函数进行包装');}function classDec2(constructor: Function) {   /    * 类装饰器2:对类装饰类的constructor做一些事情    * @param {constructor}:Function被装饰类的构造函数作为其唯一参数    */   console.log('执行类装饰器`classDec2`的语句');}@classDec1@classDec2class MyClass { attrib: string; constructor(attrib: string) {   console.log('原构造函数执行');   this.attrib = attrib; }}let cls = new MyClass("attrib value")

Out[]:

执行类装饰器`classDec2`的语句执行类装饰器`classDec1`的语句类装饰器语句执行:可在构造类前对类构造函数进行包装原构造函数执行

示例2. 使用类装饰器对类扩展
定义一个类装饰器

// 如何 扩展/装饰 一个类function classDec<T extends {new(...args:any[]):{}}> (constructor:T) {   /    * 接收被装饰类的构造函数`constructor`作为唯一参数    * 剩余参数 `...args` 接收原被装饰类构造器的参数    */   return class extends constructor {      /* 对被装饰的类进行扩展 */newProperty1 = "new property 1";   // 被扩展的新属性1newProperty2 = "new property 2";   // 被扩展的新属性2   }}

其中

  • 要扩展被装饰类的属性就需要接收以前的属性。被装饰类的构造函数仍作为唯一参数传入其类型注释为泛型,应是从之前继承而来的。剩余参数 ...args是一个数组参数(...args:any[])被装饰类构造时的参数列表。
  • 实质上{new(...args:any[]):{}}是所谓mixin 构造类型,它是有一个构造签名、带有一个类型为any[]的剩余参数(用…表示的数组型参数)和一个对象(object-like)的 返回类型。比如给定一个对象类型X,那么new (...args: any[]) => X是一个带有实例类型Xmixin 构造器类型mixin类是扩展类型参数类型表达式的类声明或表达式。
  • 更多参见Mixins与Mix-in类型

将装饰器应用到类上:

@classDec     // 将写好的装饰器应用到某个类上,如法格式为 @expressionclass MyClass1 {   property1 = "property 1";   property2: string;   constructor(property2: string) {this.property2 = property2;   }}@classDec      // 就像被声明的函数一样,同一个装饰器也能反复装饰不同的类class MyClass2 {   constructor() {      /这个类在被装饰前啥也没有 */   }}console.log(new MyClass1("property2 value"));console.log(new MyClass2());

Out[]:

MyClass1 {  property1: 'property 1',  property2: 'property2 value',  newProperty1: 'new property 1',  newProperty2: 'new property 2'}MyClass2 {  newProperty1: 'new property 1',  newProperty2: 'new property 2'}

示例3. 在装饰器中提前创建类的实例该实例:

虽然这个例子比较无聊,但是还是有助于对类装饰器用法的理解的。

let father = '小明父亲'let mother = '小明母亲'let child = '小明'// 定义一个装饰器function assign(constructor:any){   let a = Object.assign(constructor)   // 通过被装饰类的构造器拷贝该类   new a(mother,child)}@assignclass MyClass {   constructor(parent: string, child: string) {console.log(parent+'属于'+child+'的parents。')   }}new MyClass(father,child)

Out[]:

小明母亲属于小明的parents。小明父亲属于小明的parents。

示例4. 使用装饰器让类封闭。

// 定义一个装饰器function sealed(constructor: Function) {   Object.seal(constructor);}@sealedclass MyClass {   prop1 :string   prop2: string   constructor(prop1,prop2) {      this.prop1 = prop1;      this.prop2 = prop2;   }}console.log('MyClass是否是封闭的:'+Object.isSealed(MyClass));Object.defineProperty(MyClass,'prop1',{   enumerable:true})for(let i in MyClass){   console.log(i);}

当把@sealed注释掉时,运行的结果为:

MyClass是否是封闭的:falseprop1

可以看到在没有使用该装饰器时我们可以为MyClass类(也是对象)的属性使用属性描述符enumerable使之可遍历。然而使用@sealed装饰器后,语句Object.seal(constructor)通过作用类的构造函数装饰类,实际上就是使得类对应的对象封闭。这时如果再对其属性进行描述(参考JavaScript属性描述符相关内容)就会报错:

通过这样一个装饰器函数,我们只要将其装饰到任意需要封闭的类上,再不改变或重写该类就可以直接将其封闭了。从以上的例子我们看出:

  • 装饰器其实就是且必须是一个函数;
  • 使用装饰器作用到被装饰者上时,即装饰器表达式@expression,其中的expression求值后就是装饰器的函数名;
  • 装饰器在作用到被装饰者时,@expression本质上看就是装饰器函数的调用。

这里必须指出类本身就是一个对象,typescript使用并扩展ES6类语法糖。实际上在ES6后也不存在基于类的面向对象编程语言中的类,ES的以后类仍然是基于原型链的,而这些类不过是函数模拟的。底层使用装饰器装饰该类前后的JavaScript代码分别为:

使用装饰器前对应的JavaScript代码

var MyClass = / @class */ (function () {    function MyClass(prop1, prop2) { this.prop1 = prop1; this.prop2 = prop2;    }    return MyClass;}());

使用装饰器后对应的JavaScript代码

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 sealed(constructor) {    Object.seal(constructor);}// 被装饰器装饰的类 MyClass 在底层也是函数var MyClass = / @class */ (function () {    function MyClass(prop1, prop2) { this.prop1 = prop1; this.prop2 = prop2;    }    MyClass = __decorate(      [sealed],     // 所有通过`@expression`等表达式装收到类的装饰器依顺序在此数组中      MyClass    );    return MyClass;}());

2. 方法 装饰器

  • 应用到方法的 属性描述符上;
  • 用来监视,修改或者替换方法定义;
  • 接收三个参数,具体见定义格式部分;
  • 返回值:(如果有)如果方法装饰器返回一个值,它会被用作方法的属性描述符

Typescript中也可以定义类方法的装饰器,其语法格式为:

function methodDecorator(target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) {   /    * 方法装饰器:    * 类型:MethodDecorator    * @param {target}对于实例成员是类的`原型对象`,对于静态成员来说是类的`构造函数 constructor`    * @param {propertyKey}  成员的名字    * @param {descriptor}   成员的属性描述符( PropertyDescriptor )    */};

上面说到,如果方法装饰器返回一个值,它会被用作方法的属性描述符(如果代码输出目标版本小于ES5返回值会被忽略),这里简单回顾一下 属性描述符

  • PropertyDescriptor(属性描述符)用于声明一个属性是否可以写入到是否可以删除可以枚举和指定内容的对象,它是可以具有以下键值的对象:

    字段名称 描述
    getter get 语法将对象属性绑定到将在查找该属性时调用的函数
    setter set 当尝试设置该属性时,该 语法将对象属性绑定到要调用的函数
    value 描述指定属性的值,可以是任何有效的 Javascript 值(函数、对象、字符串…)
    configurable 声明该属性可以修改和删除
    enumerable 声明属性可以被枚举,可枚举属可以被for...in循环遍历
    writable 声明指定的属性是否可以重写
  • 关于属性描述符的详细内容可以点击博文链接进行查看:https://blog.csdn.net/qq_28550263/article/details/117840986#PropertyDescriptor

装饰 静态成员/实例成员 时的区别逻辑

在TypeScript类中:

  • 实例成员仅当类被实例化的时候才会被初始化的类成员;
  • 静态成员存在于类本身上(而非类的实例上)的类成员,静态成员需要用static关键字声明。

对于实例成员

class MyClass {    constructor() {    }    @methodDecoratorName    myMethod() {      return "I am method named myMethod"    }}function methodDecoratorName(target, propertyKey, descriptor){  console.log('target:', target);   // target指向方法所属类的原型  console.log('target.myMethod():', target.myMethod());  // 可以在这使用该方法  console.log('propertyKey:', propertyKey);// 该方法的方法名  console.log('descriptor:', descriptor);  // 该方法的属性描述符}let cls = new MyClass()

Out[]:

target: { myMethod: [Function (anonymous)] }target.myMethod(): I am method named myMethodpropertyKey: myMethoddescriptor: {  value: [Function (anonymous)],  writable: true,  enumerable: true,  configurable: true}

其中target输出的是{ myMethod: [Function (anonymous)] },显然这是一个对象,对象里面只有一个函数就是myMethod。这个对象就是方法所属类的原型。

从底层来看这时,MyClass部分对应的底层JavaScript(ES5语法)代码为:

var MyClass = / @class */ (function () {    function MyClass() {    }    MyClass.prototype.myMethod = function () { return "I am a static member";    };    __decorate([ methodDecoratorName    ], MyClass.prototype, "myMethod", null);    return MyClass;}());

你看到的是MyClass.prototype,这就是装饰实例成员时对应的target参数。

对于静态成员

class MyClass {    constructor() {    }    @methodDecoratorName    static myMethod(){ return "I am a static member"    }}function methodDecoratorName(target, propertyKey, descriptor){  console.log('target:', target);  console.log('target.myMethod():', target.myMethod());  console.log('propertyKey:', propertyKey);  console.log('descriptor:', descriptor);}let cls = new MyClass()

Out[]:

target: [Function: MyClass] { myMethod: [Function (anonymous)] }target.myMethod(): I am a static memberpropertyKey: myMethoddescriptor: {  value: [Function (anonymous)],  writable: true,  enumerable: true,  configurable: true}

其中target输出的是[Function: MyClass] { myMethod: [Function (anonymous)] },相比于之前的{ myMethod: [Function (anonymous)] }多了[Function: MyClass]部分。

这时,MyClass部分对应的底层JavaScript(ES5语法)代码为:

var MyClass = / @class */ (function () {    function MyClass() {    }    MyClass.myMethod = function () { return "I am a static member";    };    __decorate([ methodDecoratorName    ], MyClass, "myMethod", null);    return MyClass;}());

你看到静态成员时对应传入target参数的是MyClass而不再是MyClass.prototype。这就是两者在装饰器时的根本区别所在。

3. 装饰器工厂(以工厂模式使用 装饰器)

现实生活中“工厂”是生产同类产品的地方。类似地,“装饰器工厂”是生产具有共同地某些过程地装饰器地地方。在编程实践中我们常使用函数来实现工厂模式,其返回值也就是相当于具有相同特质的一些列产品,比如“对象”,也可以是我们这里的装饰器。面向对象编程中当我们用工厂模式生产对象时,我们可以定义一个函数让它依据给定参数不同返回有所不同的同一类特质的对象。对于装饰器亦可以使用相似的方法。装饰器工厂也一样,就是用工厂模式依据指定的参数不同返回装饰器。具体来说,我们这里的装饰器工厂是这样一个函数它返回一个函数,被返回的函数就是装饰器
实际使用时很多的情况下我们使用的都是装饰器工厂,因为我们往往需要一个这样的装饰器,它的装饰效果随着某些参数不同将得到一定的调整。
用一个方法装饰器工厂的例子来说明。当我们直接定义一个装饰器时,我们大概是上一节这个样子来写的:

function methodDecoratorName(target, propertyKey, descriptor){  ...}

这就是一个普通的装饰器,装饰到类方法之后,装饰器表达式会在运行时当作函数被调用,而似乎这个表达式也不需要其它的额外参数,所有的功能都已经随着该装饰器函数的声明确定了:

class MyClass{    @methodDecoratorName  // 装饰器表达式会在运行时当作函数被调用    myMethod(){}}

而使用工厂模式,我们会用一个函数来返回一个装饰器。这个函数我们习惯上称作工厂函数。通过给工厂函数传入不的参数,我们就有了功能相似但又不完全一样的装饰器了,这样可以降低我们代码的重复率。以 方法装饰器工厂 为例,其用法大致上是这样的:

// 伪代码class MyClass{  @decoratorFactory(各种参数)  MyMethon(){}}// 装饰器工厂,返回值类型为 MethodDecorator ,是一个用作方法装饰器的函数function decoratorFactory(...pram:any): MethodDecorator{    // 工厂的返回值是一个装饰器函数    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {  };}

再给个实例:

class MyClass{  @decoratorFactory(true)  static MyMethod1(){}  @decoratorFactory(false)  static MyMethod2(){}}// 装饰器工厂function decoratorFactory(isEnumerable:boolean):MethodDecorator{    return function (target, propertyKey, descriptor: PropertyDescriptor) {      descriptor.enumerable = isEnumerable;  };}let mcls = new MyClass();for(let i in MyClass){  console.log(i);}

Out[]:

MyMethod1

可以看到,静态方法MyMethod1()MyMethod2()被装饰器工厂作用时,由于装饰器工厂表达式处传入了不同的值,装饰器工厂为两个方法返回了不同的装饰器。前一个装饰器使得静态方法MyMethod1()的属性描述符enumerable对于为true,后者静态方法MyMethod2()对应的属性描述符enumerable对于为false,因此最后遍历MyClass的属性时,MyMethod1被遍历到,而MyMethod2不被遍历到。这样我么就不再需要分别为MyMethod1MyMethod2各写一个装饰器函数了。

再举个例子:

class MyClass{  @readOlny(true)  static MyMethod1(){}  @readOlny(false)  static MyMethod2(){}}// 装饰器工厂function readOlny(isreadOlny:boolean):MethodDecorator{    return function (target, propertyKey, descriptor: PropertyDescriptor) {      descriptor.writable = !isreadOlny;  };}

同样被装饰器工厂readOlny修饰,前者属性描述符的writable属性被置为了false,而后者被置为了true

4. 属性 装饰器

在JavaScript中不存在真正的方法,本质上对象中的函数只是另一个对象而并非存储,只不过通过一个属性引用到了这个函数。既然方法都有装饰器属性自然也有。属性装饰器的格式为:

function propDecorator(target: object, name: string | symbol){  /   * 属性装饰器   * 类型:PropertyDecorator   * @param {target}  对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。   * @param {name}    成员的名字。   */}

属性参数器和方法装饰器类似,相比更简单一些。并且属性装饰器和类装饰器、方法装饰器,以及下一节的存取器装饰器,都可以使用工厂模式的思想,这里不做重复讲解。关于其target参数,请参考方法装饰器这一节。

5. 存取器 装饰器

存取器(也称访问器)即“Getter” 和 “Setter”,用来装饰“Getter” 和 “Setter”的装饰器为所谓存取器装饰器。存取器装饰器的格式为:

function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {};

下面是一个存取器装饰器(工厂)的例子:

class Myclass {  private _a: number;  private _b: number;  constructor(a: number, b: number) {      this._a = a;      this._b = b;  }  @configurable(true)     // 可以修改和删除  get a() { return this._a; }  @configurable(false)    // 不可以修改和删除  get b() { return this._b; }  set a(num: number){    this._a = num;  }  set b(num: number){    this._b = num;  }}function configurable(value: boolean) {  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {      descriptor.configurable = value;  };}console.log(delete Myclass.prototype.a);console.log(delete Myclass.prototype.b);

Out[]:

truefalse

可以看到,经过修饰后的属性a和b,a可以从类原型中删除,而b不可以。

【注意】:
TypeScript不允许同时装饰一个成员的get和set访问器。取而代之的是,一个成员的所有装饰的必须应用在文档顺序的第一个访问器上。这是因为,在装饰器应用于一个属性描述符时,它联合了get和set访问器,而不是分开声明的。

这个要怎么理解呢?从使用上看也就是说对于一个成员我们是要装饰一次,且放在出现的第一个位置上:

class Myclass {  private _a: number;  constructor(a: number) {      this._a = a;  }  @configurable(true)     // a 的 Getter先出现,则装饰器写于此  get a() { return this._a; }    set a(num: number){     // 此处无需再用装饰器装饰    this._a = num;  }}function configurable(value: boolean) {  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {      descriptor.configurable = value;  };}

而这样写是错的:

class Myclass {  private _a: number;  constructor(a: number) {      this._a = a;  }  @configurable(true)     // a 的 Getter先出现,则装饰器写于此  get a() { return this._a; }  @configurable(true)     // 错误写法  set a(num: number){  this._a = num;  }}

在VSCode编辑器中你会看到错误提示:
在这里插入图片描述
实际上,在底层的JavaScript代码是这样的:

// 用函数模拟类var Myclass = / @class */ (function () {    function Myclass(a) { this._a = a;    }    Object.defineProperty(Myclass.prototype, "a", { get: function () { return this._a; }, set: function (num) {     this._a = num; }, enumerable: false, configurable: true    });    __decorate([ configurable(true)  // 不论 a 的 Getter 还是 Setter ,同一// 装饰器函数都在此数组中传入,只需一次// 因此对同一各属性,在TypeScript中我们// 只将装饰器写在文本顺序靠前的一个Getter// 或Setter上。    ], Myclass.prototype, "a", null);    return Myclass;}());// 我们定义的装饰器函数function configurable(value) {    return function (target, propertyKey, descriptor) { descriptor.configurable = value;    };}

6. 补充说明:

(1)关于元数据

根据官方说法,装饰器元数据是个实验性的特性并且可能在以后的版本中发生破坏性的改变。因此不做讲解。

(2)多个装饰器表达式的写法

装饰器作用在被装饰目标上时,除了文中提到的写法也可以又其它写法。多个装饰器可以同时应用到一个声明上,就像下面的示例:

  • 书写在同一行上:
@f @g 被装饰的目标
  • 书写在多行上:
@f@g被装饰的目标

当多个装饰器应用于一个声明上,它们求值方式与复合函数相似。在这个模型下,当复合fg时,复合的结果(fg)(x)等同于f(g(x))。

(3)多个装饰器应用在一个目标上时的优先顺序:

  • 同目标上的装饰器优先顺序

    • 由上至下依次对装饰器表达式求值。
    • 求值的结果会被当作函数,由下至上依次调用。
  • 类中不同目标上的装饰器将按以下规定的顺序应用

    • 参数装饰器,然后依次是方法装饰器,访问符装饰器,或属性装饰器应用到每个实例成员。
    • 参数装饰器,然后依次是方法装饰器,访问符装饰器,或属性装饰器应用到每个静态成员。
    • 参数装饰器应用到构造函数。
    • 类装饰器应用到类