# 装饰器

除了采用继承和混入对类进行扩展,装饰器语法提供了一种新的方式。使用装饰器前确保, tsconfig.json 中打开了 experimentalDecorators 配置。

{
    //...
    "compilerOptions": {
        // 打开装饰器配置
        "experimentalDecorators": true
        // ...
    }
}

# 装饰器概述

装饰器是处于 草案阶段 stage2 (opens new window) 的 ECMAScript 语法,Typescript 基于此草案实现了改特性。

装饰器语法实现了对类的动态扩充,避免了直接对类的修改。是实现 面向切面编程 AOP (opens new window) 的一种手段。

TypeScript 提供了一系列装饰器语法来实现对类的动态修改,TypeScript 和 js 草案在装饰器实现上有差异 罗列如下:

装饰器 功能 ts js 草案
类装饰器 扩充类 ✅ ✅
类方法装饰器 修改类的方法 ✅ ✅
类静态方法装饰器 修改类的静态方法 ✅ ✅
类属性装饰器 修改类的属性 ✅ ✅
类静态属性装饰器 修改类的静态属性 ✅ ✅
类访问器属性装饰器 修改类的访问器属性 ✅ ✅
类参数装饰器 修改类方法的参数 ✅ ⬜
装饰器工厂 动态设置装饰器 ✅ ✅
装饰器组合 混合多个装饰器 ✅ ✅

为什么没有函数的装饰器,详细讨论参见 decorators for functions (opens new window), 目前原因为函数提升导致了函数装饰器的不易实现,github 存在如下的 function-decorators-proposal (opens new window) 解决此问题

# 语法详解

# 类装饰器

// TODO: 此处扩展示例需修改
// 1. 定义类装饰器函数,传入为类构造器
function testAble(target: any) {
  target._test = true;
}


/** 
 * 2. 在类之前采用 @<函数名> 生命类装饰器
 * 改代码会在代码运行时执行如下逻辑
 * class Class1 {};
 * testAble(Class1); // 动态增加属性
 * */ 

@testAble
class Class1 {

}

console.log(Class1)

也可以返回一个新的类

// TODO: 此处扩展示例需修改
// 1. 定义装饰器返回新的构造器
function classDecorator<T extends { new (...args: any[]): {} }>(
  constructor: T
) {
  return class extends constructor {
    newProperty = "new property";
  };
}


@classDecorator
class Class2 {
  a = 1;

}

console.log(Class2)
console.log(new Class2())

# 方法装饰器

// TODO: 此处扩展示例需修改
// 1. 定义类装饰器函数,传入为类构造器
function dicription(target: any, key: String) {
  console.log(`decrption method ${key}`, arguments);
}


class Class3 {
  @dicription
  /** 
   * 2. 在方法之前采用 @<函数名> 设置装饰器
   * 改代码会在运行时,效果如下
   * class Class3 { method1() { // ... } };
   * dicription(Class3.prototype,'method1',Object.getOwnPropertyDescriptor('method1') )
   * */ 
  method1() {
    console.log('method1')
  }
}

console.log(Class3)
console.log(new Class3().method1())

若想在函数运行时也执行装饰器的注入逻辑,可修改方法描述对象的 value 值

// TODO: 此处扩展示例需修改
// 1. 定义类装饰器函数,传入为类构造器
function dicription1(target: any, key: PropertyKey , val: PropertyDescriptor) {
  console.log('run in init', key, val);
  let { value: method } = val;
  function change(this: Class4) {
    console.log('run in method call', this);
    console.log(`decrption method ${key as string}`, arguments);
    // 注意此处采用 call 防止 this 丢失
    method.call(this, ...arguments)
  }
  // 修改原始函数
  val.value = change
  console.log('change method success', Object.getOwnPropertyDescriptor(target, key))
}


class Class4 {
  /** 
   * 2. 在方法之前采用 @<函数名> 设置装饰器
   * 改代码会在运行时,效果如下
   * class Class3 { method1() { // ... } };
   * dicription(Class3.prototype,'method1',Object.getOwnPropertyDescriptor('method1') )
   * */ 
  @dicription1
  method1(...args:any) {
    console.log('method1', args, this.method1)
  }
}

console.log(Class4)
console.log(new Class4().method1(1,2,3))

TIP

注意示例中高亮行,修改方法运行时需避免 this 的丢失

也可直接返回修改的方法描述对象

// TODO: 此处扩展示例需修改
// 1. 定义类装饰器函数,传入为类构造器
function dicription(target: any, key: PropertyKey , val: PropertyDescriptor) {
  return {
    value: function() {
      console.log('run in method call', this);
      console.log(`decrption method ${key as string}`, arguments);
      // 注意此处采用 call 防止 this 丢失
      return val.value.call(this, ...arguments)
    }
  }
}


class ClassTest {
  /** 
   * 2. 在方法之前采用 @<函数名> 设置装饰器
   * 改代码会在运行时,效果如下
   * class Class3 { method1() { // ... } };
   * dicription(Class3.prototype,'method1',Object.getOwnPropertyDescriptor('method1') )
   * */ 
  @dicription
  method1(...args:any) {
    console.log('method1', args, this.method1)
  }
}

console.log(ClassTest)
console.log(new ClassTest().method1(1,2,3))

# 静态方法

除了类方法,装饰器也支持对静态方法的修改

// TODO: 此处扩展示例需修改
// 1. 定义类装饰器函数,传入为类构造器
function dicription2(target: any, key: String, val: PropertyDescriptor) {
  console.log(`decrption method ${key}`, arguments);
}


class Class5 {
  /** 
   * 2. 在方法之前采用 @<函数名> 设置装饰器
   * 改代码会在运行时,效果如下
   * class Class5 { method1() { // ... } };
   * dicription(Class5,'method1',Object.getOwnPropertyDescriptor('method1') )
   * */ 
  @dicription2
  static method1() {
    console.log('method1')
  }
}

console.log(Class5)
console.log(Class5.method1())

TIP

注意静态方法装饰器的 target 为类,而非类的原型,一个简单的记忆原则是, target 为修改内容的所属者,例如方法属于类的原型所以 target 为类,静态方式属于类 所以 target 为类,该规则适用于所有装饰器

也可采用修改属性描述符,实现在调用时触发相关逻辑

// TODO: 此处扩展示例需修改
// 1. 定义类装饰器函数,传入为类构造器
function dicription3(target: any, key: PropertyKey , val: PropertyDescriptor) {
  console.log('run in init', key, val);
  let { value: method } = val;
  function change(this: any) {
    console.log('run in method call', this);
    console.log(`decrption method ${key as string}`, arguments);
    // 注意此处采用 call 防止 this 丢失
    method.call(this, ...arguments)
  }
  // 修改原始函数
  val.value = change
  console.log('change method success', Object.getOwnPropertyDescriptor(target, key))
}


class Class6 {
  @dicription3
  static method1(...args:any) {
    console.log('method1', args, this.method1)
  }
}

console.log(Class6)
console.log(Class6.method1(1,2,3))

# 属性装饰器

/**
 * 注意由于属性值在实例化时才存在,所以无 value 参数
 * @param target 类的原型,由于实例在初始化时才创建,此处用原型替代
 * @param key 属性值
 */
function log(target:any, key: PropertyKey):any {
    // 修改属性为只读
    console.log('run in init');
    let val: any;
    return {
      set: function (value: any) {
        val = value;
        console.log(`Set ${key as string} to ${value}`, this);
      },
      get: function() {
        return val;
      }
    };
}

class Class8  {
    @log
    name: string;
    constructor(name : string) {
        this.name = name
    }
}
let c = new Class8 ('tom');
console.log("%o", c)

由于属性在实例化时才创建,而装饰器在类声明时就运行,如何实现在创建类时才执行描述符定义。 这里利用了 Reflect.decorate 方法。

TIP

注意定义属性装饰器时不要传入 value 参数, ts 检查会报错, 详见 Why I can't use decorator on property (opens new window)

# 静态属性装饰器

静态属性也可使用装饰器

function log(target:any, key: PropertyKey, descriptor: any): any{
  console.log('run init', arguments)
  let oldGetter = descriptor.get
  return {
    get: function () {
      console.log('log', descriptor, this[key])
      // 报错原因
      oldGetter()
    }
  }
}

class Class10  {
    get fullName():string {
      return this.firstName + this.lastName
    }
    firstName: string;
    lastName: string;
    constructor (firstName: string, lastName: string) {
      this.firstName = firstName;
      this.lastName = lastName;

    }
}

// 对只读属性重写报错
let user = new Class10('hello','world');
user.fullName 

# 访问器属性装饰器

也可设置访问器属性为装饰器。

function readonly(target:any, key: PropertyKey): any{
  console.log('run init', arguments)
  return {
    writable: false
  }
}

class Class9  {
    @readonly
    get name()
}

// 对只读属性重写报错
Class9.username = 'jack'
console.log(Class9.username)

# 参数装饰器

参数装饰器,注意参数装饰器传入的第三个参数为 index, 参数对应索引值。

// 1. 定义参数装饰器
function requireVal(target: any,key: PropertyKey,value: any):any {
  let d = Object.getOwnPropertyDescriptor(target,key)
  console.log('init run', arguments, target, d)
  Object.defineProperty(target, key, {
    value: function() {
      if(!arguments[value])  {
        console.log('must have name')
      }
      return d?.value.apply(this,arguments)
    }
  })
}

class Greeter2 {
  greet(@requireVal name?: string) {
    return "Hello " + name;
  }
}

// 运行时会检查属性值, TODO: tsnode 为什么失败
console.log(new Greeter2().greet())
console.log(new Greeter2().greet('tom'))

# 装饰器工厂

利用装饰工厂动态创建装饰器,返回的装饰器会在类申明时运行。

// 1. 申明装饰器工厂返回装饰器函数
function testAble(flag: Boolean) {
  return function(target: any) {
    target._test = flag
  }
}

// 返回类构造器,类申明时,设置类的 _test 为 false
@testAble(false)
class Class11 {
}
console.log(Class11)

// 返回类构造器,类申明时,设置类的 _test 为 true
@testAble(true)
class Class12 {

}
console.log(Class12)

# 装饰器组合

装饰器可以叠加使用。

function d1():any {
  console.log("d1(): evaluated");
  return function (
    target: any,
    key: PropertyKey,
    val: PropertyDecorator
  ) {
    console.log("d1(): called");
  };
}

function d2():any {
  console.log("d2(): evaluated");
  return function (
    target: any,
    key: PropertyKey,
    val: PropertyDecorator
  ) {
    console.log("d2(): called");
  };
}


/**
 * tsc 编译后的执行结果类似
 * let d1Res = d1()
 * let d2Res = d1()
 * // 靠近方法的装饰器先执行
 * d2Res(target,key,value)
 * d1Res(target,key,value)
 */
class Class14 {
  @d1()
  @d2()
  method() {}
}

// return 
// d1(): evaluated
// d2(): evaluated
// d2(): called
// d1(): called

注意组合装饰器是的执行顺序是从内往外。

# 注解(Annotations) 和装饰器 (decorator)

参考 The difference between Annotations and Decorators (opens new window) 注解为类提供额外的注释信息,可以采用装饰器语法实现该功能。

# 使用场景

# 元编程

typescript 内置了一些装饰器通过 emitDecoratorMetadata 配置打开。

{
  "compilerOptions": {
    "emitDecoratorMetadata": true
  }
}

该配置会内置 reflect-metadata (opens new window) 相关装饰器

# 总结

  1. 装饰器是一个函数,用于实现对类的动态修改,基本结构如下
    function decorator(target, key ,value) {
        // 可以返回新的类或属性描述对象
    }
    
    
  2. 类的装饰器入参为类,支持返回新的类替换原有类
  3. 方法装饰器,入参为 类的原型,方法名,方法描述符,通过修改方法描述符实现运行时修改,支持返回新的属性描述符进行替换
  4. 静态方法装饰器,入参的第一个参数为类,其他功能和方法装饰器类似
  5. 属性装饰器,入参为 类的原型,属性名
  6. 静态属性装饰器,入参为 类的原型,属性名
  7. 访问器属性装饰器,入参为 类的原型,属性名
  8. 装饰器工厂,返回装饰器用于实现动态设置装饰器
  9. 多重装饰器,装饰器的叠加使用
  10. 使用场景
  11. 类装饰器,动态扩充方法,混入
  12. 属性装饰器,
    1. 格式化属性值
    2. 校验属性值
  13. 方法装饰器
    1. 废弃方法申明
    2. 方法只读
  14. 额外注意事项
    1. 装饰器的运行时机是在类申明后,可以通过修改属性描述符实现方法运行时的动态插入

# 参考资料