热搜:前端 nest neovim nvim

精进 TS 和 Nest 必备的装饰器

lxf2023-10-24 16:30:01
  • 装饰器:对类对象,方法,属性进行包装

  • 一种能力增强(类似游戏里面加buff)

  • JS 中的装饰器其实就是函数,使用:@+函数名

  • 装饰器对类的改变,是在代码编译时发生的,而不是在运行时

类装饰器

基本使用

  • 类装饰器接受一个参数,target 参数指的是类本身
function addFly(target) {
  target.prototype.isFly = true;
  target.isFly = true;
}

@addFly
class Man {
  name = ""
  constructor(name) {
    this.name = name;
  }
}

console.log(Man.isFly); // true
const man = new Man("云牧");
console.log(man.isFly); // true

传参以及多个执行顺序

  • 由上到下依次对装饰器表达式求值
  • 求值的结果会被当作函数,由下至上调用
function addFly1() {
  console.log("addFly1");
  return function (target) {
    console.log("addFly1 执行");
    target.prototype.addFly1 = true;
  };
}

function addFly2(height) {
  console.log("addFly2");
  return function (target) {
    console.log("addFly2 执行");
    target.prototype.addFly2Fun = function () {
      console.log("飞行高度:", height);
    };
  };
}

function addFly3() {
  console.log("addFly3");
  return function (target) {
    console.log("addFly3 执行");
    target.prototype.addFly3 = true;
  };
}

@addFly1()
@addFly2(300)
@addFly3()
class Man {
  name = "";
  constructor(name) {
    this.name = name;
  }
}

const man = new Man("云牧");
console.log(man.addFly1); // true
man.addFly2Fun(); // 飞行高度: 300
console.log(man.addFly3); // true

执行顺序:

addFly1
addFly2
addFly3
addFly3 执行
addFly2 执行
addFly1 执行
true
飞行高度: 300
true

重载构造

function classDecorator(constructor) {
  return class extends constructor {
    name = "云牧";
  };
}

@classDecorator
class Man {
  name = "";
  constructor(name) {
    this.name = name;
  }
}

const man = new Man("黛玉");
console.log(man); // Man { name: '云牧' }

方法装饰器

接受三个参数:

  • target - 目标对象
  • name - 属性名
  • descriptor - 属性描述符

备注:装饰器的本质是利用了 ES5 的 Object.defineProperty 属性,所以这三个参数其实是和 Object.cdefineProperty 参数是一致的

无参

function methodReadonly(target, key, descriptor) {
  console.log(target, key, descriptor);
  // {}, toString, { value: [Function: toString],  writable: true, enumerable: false, configurable: true }

  descriptor.writable = false;
  descriptor.configurable = false;
  return descriptor;
}

class Man {
  name = "";
  constructor(name) {
    this.name = name;
  }

  @methodReadonly
  toString() {}
}

const man = new Man("云牧");

有参

function methodDecorator(moreHp = 0) {
  return function (target, key, descriptor) {
    // 获取原来的方法
    const originalMethod = descriptor.value;

    // 重写该方法
    descriptor.value = function (...args) {
      console.log(args); // [ '云牧', 10 ]
      args[1] = args[1] + moreHp;

      return originalMethod.apply(this, args);
    };

    return descriptor;
  };
}

class Man {
  name = "";
  hp = 0;

  constructor(name, hp) {
    this.init(name, hp);
  }

  @methodDecorator(50)
  init(name, hp) {
    this.name = name;
    this.hp = hp;
  }
}

const man = new Man("云牧", 10);
console.log(man); // Man { name: '云牧', hp: 60 }

访问器装饰器

  • 访问器装饰器和方法装饰器的参数一致
function minHp(minValue) {
  return function (target, key, descriptor) {
    const originalSet = descriptor.set;

    descriptor.set = function (value) {
      if(value <= minValue) return;
      
      // 执行原先的 set 方法
      originalSet.call(this, value)
    };
  };
}

class Man {
  name = "";
  hp = 0;

  constructor(name, hp) {
    this.init(name, hp);
  }

  init(name, hp) {
    this.name = name;
    this.hp = hp;
  }
  
  @minHp(0)
  set setHp(value) {
    this.hp = value;
  }
}

const man = new Man("云牧", 10);
man.setHp = -10; // 修改不成功
console.log(man); // Man { name: '云牧', hp: 10 }

属性装饰器

  • JS 属性装饰器和方法装饰器的参数一致
  • 注意 TypeScript 与 JavaScript 的装饰器实现是不一致的,ts属性装饰器只有前两个参数

无参

function propertyReadonly(target, propertyName, descriptor) {
  console.log(target, propertyName, descriptor);
  // {}, name, { configurable: true, enumerable: true, writable: true, initializer: [Function: initializer] }

  descriptor.writable = false;
}

class Man {
  @propertyReadonly name = "default name";
  constructor(name) {
    this.name = name;
  }
}

const man = new Man("云牧");
man.name = "黛玉"; // 修改失败
console.log(man);

有参

function checkType(type) {
  return function (target, propertyName, descriptor) {
    console.log(descriptor.initializer); // function () { return "default name"; }

    let value = descriptor.initializer?.call(this);

    return {
      enumerable: true,
      configurable: true,
      get: function () {
        return value;
      },
      set: function (v) {
        if (typeof v === type) {
          value = v;
        }
      }
    };
  };
}

class Man {
  @checkType("string")
  name = "default name";

  constructor(name) {
    this.name = name;
  }

  setName(name) {
    this.name = name;
  }

  getName() {
    return this.name;
  }
}

const man = new Man("云牧");
man.setName(123);
console.log(man.getName()); // 云牧

装饰器加载顺序

  • 总体表达式求值顺序:类装饰器 > 属性装饰器,方法装饰器
  • 求值结果:属性装饰器,方法装饰器 > 类装饰器
  • 属性与方法装饰器,谁在前面谁先执行
  • 表达式从上往下,求值结果从下往上
function classDecorator1() {
  console.log("classDecorator1");
  return function (target) {
    console.log("classDecorator1 执行");
  };
}

function classDecorator2() {
  console.log("classDecorator2");
  return function (target) {
    console.log("classDecorator2 执行");
  };
}

function methodDecorator1() {
  console.log("methodDecorator1");
  return function (target) {
    console.log("methodDecorator1 执行");
  };
}

function methodDecorator2() {
  console.log("methodDecorator2");
  return function (target, key, descriptor) {
    console.log("methodDecorator2 执行");
    return descriptor;
  };
}

function propertyDecorator1() {
  console.log("propertyDecorator1");
  return function (target, key, descriptor) {
    console.log("propertyDecorator1 执行");
    return descriptor;
  };
}

function propertyDecorator2() {
  console.log("propertyDecorator2");
  return function (target, key, descriptor) {
    console.log("propertyDecorator2 执行");
    return descriptor;
  };
}

@classDecorator1()
@classDecorator2()
class Man {
  @propertyDecorator1()
  @propertyDecorator2()
  name = "";
  hp = 0;

  constructor(name, hp) {
    this.init(name, hp);
  }

  init(name, hp) {
    this.name = name;
    this.hp = hp;
  }
  @methodDecorator1()
  @methodDecorator2()
  run() {
    console.log("跑步");
  }
}

const main = new Man("云牧", 10);

执行结果:

classDecorator1
classDecorator2
propertyDecorator1
propertyDecorator2
methodDecorator1
methodDecorator2
propertyDecorator2 执行
propertyDecorator1 执行
methodDecorator2 执行
methodDecorator1 执行
classDecorator2 执行
classDecorator1 执行

注意问题

  • Decorator 叠加使用没问题,因为你的每次包装,都会将属性的 descriptor 返回给上一层的包装,最后就是一个函数包函数包函数的效果,最终返回的还是这个属性的 descriptor
  • 装饰器只能用于类和类方法,不能用于普通函数

装饰器模式-AOP实现(面向切面编程)

  • 定义:给对象动态增加职责,并不真正改动对象自身
  • 简单来说就是:包装现有的模块,使之更加强大,并不会影响原有接口的功能,可以理解为锦上添花
  • ES7 装饰器是AOP的一种实现,可以很方便的实现装饰器模式,并且甚至可以改变类和方法的原有功能

完成一个需求,用户登录后打印日志

const showLogin = function () {
  console.log("用户登录");
  log();
};

使用装饰器模式:

Function.prototype.afterLog = function (afterFn) {
  return (...args) => {
    const res = this.apply(this, args);
    afterFn.apply(this, args);
    return res;
  };
};

let showLogin = function (name) {
  console.log("用户登录", name);
};

const log = function (name) {
  console.log("上报日志", name);
};

showLogin = showLogin.afterLog(log);

showLogin("云牧");
// 用户登录 云牧
// 上报日志 云牧

应用场景

  • 异常处理
  • 日志记录
  • 与工厂模式或者装饰器模式结合
  • react 中封装 @connect 装饰器等
  • 节流,防抖等装饰器
  • ...