概念

装修器是一个在代码运行时动态增加功用的方式,它能够用来修正类或函数的行为,通常用于:

  1. 扩展已有的类或函数功用;
  2. 修正类或函数的特点;
  3. 将类或函数转化为不同的方式,例如,将类转化为单例形式。

JavaScript 中,装修器的完成有很多种方式,其间比较常用的方式是运用装修器函数和类装修器。

运用装修器函数

能够运用一个装修器函数来修正类或函数的行为,装修器函数接纳传入的类或函数作为参数,并将修正后的类或函数返回。例如,下面的比如演示了怎么运用装修器函数来给一个类增加 log 函数:

function addLogFunction(cls) {
  cls.prototype.log = function(msg) {
    console.log(`[${new Date().toISOString()}] ${msg}`);
  };
  return cls;
}
@addLogFunction
class MyClass {
  constructor() {}
}
const myObj = new MyClass();
myObj.log('hello');

在这个比如中,addLogFunction 函数接纳一个类作为参数,在该函数中将类的原型(prototype)目标上增加一个 log 办法。然后返回修正后的类。在声明 MyClass 时运用了装修器函数 @addLogFunction,相当于履行 MyClass = addLogFunction(MyClass)。当实例化 MyClass 的目标之后,调用 myObj.log('hello') 能够输出 log 信息。

运用类装修器

类装修器是一个润饰类的类,它能够修正类的行为、静态特点、原型特点等。一个类装修器能够接纳三个参数:

  1. 构造函数;
  2. 类的称号;
  3. 类的描绘目标。

下面是一个比如,运用类装修器为类增加一个静态特点:

function addVersion(cls) {
  cls.version = '1.0';
  return cls;
}
@addVersion
class MyClass {}
console.log(MyClass.version); // 输出 1.0

在这个比如中,addVersion 类装修器接纳一个构造函数作为参数,它在该构造函数上增加了一个静态特点 version,并将修正后的构造函数返回。在声明 MyClass 时运用了装修器函数 @addVersion,相当于履行 MyClass = addVersion(MyClass)。这样,就能够通过调用 MyClass.version 拜访静态特点 version。

常见的装修器应用

下面是一些常见的运用装修器的场景:

路由恳求办法装修器

function routeMethod(method) {
  return function(target, key, descriptor) {
    target.routes = target.routes || {};
    target.routes[key] = method;
    return descriptor;
  };
}
class UserController {
  @routeMethod('GET')
  getUser(id) {
    // ...
  }
  @routeMethod('DELETE')
  deleteUser(id) {
    // ...
  }
}
console.log(UserController.routes);
// 输出 {getUser: "GET", deleteUser: "DELETE"}

这个比如中,运用了 routeMethod 装修器润饰了 getUserdeleteUser 函数,给同一个类中的两个办法增加了路由恳求办法类型。

单例形式装修器

function singleton(cls) {
  let instance;
  return function() {
    if (!instance) {
      instance = new cls(...arguments);
    }
    return instance;
  };
}
@singleton
class MyClass {
  constructor(val) {
    this.val = val;
  }
}
const a = new MyClass(1);
const b = new MyClass(2);
console.log(a === b); // 输出 true

这个比如中,运用了 singleton 装修器润饰了 MyClass 类,使得该类实例化后一直返回同一个实例,从而完成了单例形式。

主动绑定 this 装修器

function autobind(_, _2, descriptor) {
  const { value: fn, configurable, enumerable } = descriptor;
  return {
    configurable,
    enumerable,
    get() {
      const boundFn = fn.bind(this);
      Object.defineProperty(this, key, {
        value: boundFn,
        configurable: true,
        writable: true,
      });
      return boundFn;
    },
  };
}
class MyComponent {
  constructor(props) {
    this.props = props;
  }
  @autobind
  handleClick() {
    console.log(this.props);
  }
}

这个比如中,运用了 autobind 装修器润饰了 handleClick 函数,使得该函数在被调用时主动绑定 this,并返回一个新的函数。这样,在实例化 MyComponent 后,调用 this.handleClick() 函数时,不需要再手动绑定 this。

日志记载

装修器能够用于记载日志,包括打印函数调用,函数履行时间等信息。

function log(target, name, descriptor) {
  const originalMethod = descriptor.value;
  descriptor.value = function (...args) {
    console.log(`Function ${name} called with ${args}`);
    const start = performance.now();
    const result = originalMethod.apply(this, args);
    const duration = performance.now() - start;
    console.log(`Function ${name} completed in ${duration}ms`);
    return result;
  };
  return descriptor
}
class MyClass {
  @log
  myMethod(arg1, arg2) {
    return arg1 + arg2;
  }
}
const obj = new MyClass();
obj.myMethod(1, 2); // Output: 
// Function myMethod called with 1,2
// Function myMethod completed in 0.013614237010165215ms

认证鉴权

装修器还能够用于查看用户的认证状况和权限,以防止未授权的用户拜访敏感数据或定期履行操作。

function authorization(target, name, descriptor) {
  const originalMethod = descriptor.value;
  descriptor.value = function(...args) {
    if (!this.isAuthenticated()) {
      console.error('Access denied! Not authenticated');
      return;
    }
    if (!this.hasAccessTo(name)) {
      console.error(`Access denied! User does not have permission to ${name}`);
      return;
    }
    return originalMethod.apply(this, args);
  };
  return descriptor;
}
class MyApi {
  isAuthenticated() {
    // perform authentication check
    return true;
  }
  hasAccessTo(endpoint) {
    // perform authorization check
    return true;
  }
  @authorization
  getUsers() {
    // return users data
  }
  @authorization
  deleteUser(id) {
    // delete user with id
  }
}

缓存

装修器还能够用于缓存函数的履行成果,以防止重复核算。

function memoize(target, name, descriptor) {
  const originalMethod = descriptor.value;
  const cache = new Map();
  descriptor.value = function (...args) {
    const cacheKey = args.toString();
    if (cache.has(cacheKey)) {
      console.log(`cache hit: ${cacheKey}`);
      return cache.get(cacheKey);
    }
    const result = originalMethod.apply(this, args);
    console.log(`cache miss: ${cacheKey}`);
    cache.set(cacheKey, result);
    return result;
  };
  return descriptor;
}
class MyMath {
  @memoize
  calculate(num) {
    console.log('calculate called');
    return num * 2;
  }
}
const math = new MyMath();
console.log(math.calculate(10)); // Output: 
// calculate called
// cache miss: 10
// 20
console.log(math.calculate(10)); // Output: 
// cache hit: 10
// 20

面向切面编程

装修器能够用于完成面向切面编程,即在不修正原始代码的情况下,在运行时增加功用。

function validate(target, name, descriptor) {
  const originalMethod = descriptor.value;
  descriptor.value = function (...args) {
    const isValid = args.every(arg => typeof arg === 'string' && arg.length > 0);
    if (!isValid) {
      console.error('Invalid arguments');
      return;
    }
    return originalMethod.apply(this, args);
  };
  return descriptor;
}
class MyForm {
  @validate
  submit(name, email, message) {
    // submit the form
  }
}
const form = new MyForm();
form.submit('', 'john@example.com', 'Hello world'); // Output: Invalid arguments

可逆装修器

装修器还能够应用在可逆的场景中,例如能够增加一个可逆的装修器来修正函数行为。

function reverse(target, name, descriptor) {
  const originalMethod = descriptor.value;
  descriptor.value = function (...args) {
    args.reverse();
    return originalMethod.apply(this, args);
  };
  return descriptor;
}
class MyMath {
  @reverse
  calculate(num1, num2) {
    return num1 + num2;
  }
}
const math = new MyMath();
console.log(math.calculate(1, 2)); // Output: 3
console.log(math.calculate.reversed(1, 2)); // Output: 3

主动类型查看

装修器能够应用在主动类型查看上,例如能够增加一个装修器来确保函数参数的类型是正确的。

function checkType(expectedType) {
  return function(target, name, descriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function (...args) {
      const invalidArgs = args.filter(arg => typeof arg !== expectedType);
      if (invalidArgs.length > 0) {
        console.error(`Invalid arguments: ${invalidArgs}`);
        return;
      }
      return originalMethod.apply(this, args);
    };
    return descriptor;
  }
}
class MyMath {
  @checkType('number')
  add(num1, num2) {
    return num1 + num2;
  }
}
const math = new MyMath();
math.add(1, '2'); // Output: Invalid arguments: 2