likes
comments
collection
share

JS装饰器,看这一篇就够了

作者站长头像
站长
· 阅读数 10

概念

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

  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