JavaScript中AOP的实现

1,525 阅读2分钟

1. 概念

AOP - Aspect oriented programming 面向切面编程。把一些跟核心业务逻辑模块无关的功能抽离出来,再通过“动态织入”的方式掺入业务逻辑模块中。

主要有以下几类使用场景:

  • 分离业务代码和统计代码 (日志、埋点等)
  • 分离表单校验和提交
  • 参数校验与更改
  • 性能分析、统计函数执行时间
  • 不更改其他人代码的基础上增加自己的功能逻辑。

2. Function.prototype.before 和 Function.prototype.after

其实所有的需求都可以概括为一类,那就是在主代码逻辑之前,或之后增加一个方法,这个方法可以处理与主代码无关的其他逻辑。那么可以在Function的原型上挂载两个方法,每个需要AOP的函数执行前通过这两个方法重新包裹以下,就可以达到想要的目的。

Function.prototype.before = function (beforeFn) {
  //存储当前this
  const self = this;
  return function () {
    //调用主函数前先调用before函数,可以执行统计、埋点操作
    //传递参数,可以进行参数的检验与修改
    beforeFn.apply(this, arguments);
    //执行主函数,并返回结果
    return self.apply(this, arguments);
  }
}
Function.prototype.after = function (afterFn) {
  const self = this;
  return function () {
    //与before方法类似,只是先调用了主函数
    let ret = self.apply(this, arguments);
    afterFn.apply(this, arguments);
    return ret;
  }
}

function mainFn (options) {
  console.log(`执行了main, ${options.name}, ${options.age}`);
}
function beforeFn (options) {
  options.name = options.name || 'nan';
  options.age = options.age || 18;
  console.log(`执行了before`);
}
function afterFn () {
  console.log(`执行了after`);
}
mainFn = mainFn.before(beforeFn).after(afterFn);
mainFn({name: 'nan'});
// 执行了before
// 执行了main, nan, 18
// 执行了after

before和after其实就是在原型上定义一个高阶函数,在返回的函数里面按照顺序去执行相应的逻辑。 在调用主函数的时候先去调用原型上的before和after方法,重新赋给主函数。

3. ES6 装饰器

AOP在ES6中可以用装饰器实现。

function before(beforeFn = function () {}) {
  return function (target, name, descriptor) {
    let oldValue = descriptor.value;

    descriptor.value = function () {
      beforeFn.apply(this, arguments);
      return oldValue.apply(this, arguments);
    };

    return descriptor;
  }
}
function beforeLog() {
  console.log('beforeLog');
}
class Main {
  constructor(val) {
    this.val = val;
  }
  @before(beforeLog)
  getVal() {
    console.log('Main getVal')
    return this.val;
  }
}

方法上的装饰器接受3个参数,

  • target 类原型 Main.prototype
  • name 要装饰的属性名 getVal
  • descriptor 该属性的描述对象
{
  value: getValFunction,
  enumerable: false,
  configurable: true,
  writable: true
}