如何深入理解 JavaScript 中的装饰器

1ab081cbca9453814614c4f20039f88c.png

Midjourney 创作,未来开发者

装饰器是一种动态地在代码运行时添加功能的方法,它可以用于修改类或函数的行为。换句话说,它可以让我们在不改变原有代码的情况下,在运行时给函数或类添加一些额外的功能。

概念

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

  • 扩展现有类或函数的功能;

  • 修改类或函数的属性;

  • 将类或函数转换为不同的形式,例如单例模式。

在 JavaScript 中,有许多实现装饰器的方式,其中最常见的是使用装饰器函数和类装饰器。

使用装饰器函数

您可以通过使用装饰器函数来修改类或函数的行为,该函数接收传入的类或函数作为参数,并返回修改后的类或函数。例如,下面的示例演示了如何使用装饰器函数向类添加一个日志函数:

function addLogFunction(cls) {
  // 向传入的类的原型上添加 log 方法
  cls.prototype.log = function(msg) {
    console.log(`[${new Date().toISOString()}] ${msg}`);
  };
  return cls;
}

@addLogFunction
class MyClass {
  constructor() {}
}

const myObj = new MyClass();
myObj.log('hello'); // 输出:[2023-03-29T06:00:00.000Z] hello

在这个示例中,函数 addLogFunction 接收一个类作为参数,在函数中将一个方法添加到该类的原型 (prototype) 对象中。然后返回修改后的类。使用装饰器函数 @addLogFunction 来声明 MyClass 类,这相当于执行了 MyClass = addLogFunction(MyClass)。当实例化 MyClass 对象时,调用 log 方法即可输出日志信息。因此代码的执行顺序为:

  • 首先,将 MyClass 作为参数传递给 addLogFunction 函数;

  • 然后,在 addLogFunction 函数中,向 MyClass 的原型添加了一个 log 方法;

  • 接着,返回修改后的 MyClass 类,并将其赋值给 MyClass 变量;

  • 最后,创建 MyClass 的实例 myObj,并调用其 log 方法,输出带有时间戳的消息。

因此,代码 myObj.log('hello') 的执行结果是:输出带有时间戳的消息 [2023-03-29T06:00:00.000Z] hello。

使用类装饰器

类装饰器是一个用于修改类的类,可以修改类、静态属性、原型属性等行为。类装饰器可以接收三个参数:

  • constructor;

  • 类的名称;

  • 类的描述对象。

以下是使用类装饰器向类添加静态属性的示例代码:

function addVersion(cls) {
  // 向传入的类添加 version 静态属性
  cls.version = '1.0';
  return cls;
}

@addVersion
class MyClass {}

console.log(MyClass.version); // 输出 1.0

在这个示例中,类装饰器接收一个构造函数作为参数,向这个构造函数添加一个静态属性 version,并返回修改后的构造函数。使用装饰器函数 @addVersion 来声明 MyClass 类,这相当于执行了 MyClass = addVersion(MyClass)。这样就可以通过调用 MyClass.version 来访问静态属性 version 的值。因此代码的执行顺序为:

  • 首先,将 MyClass 作为参数传递给 addVersion 函数;

  • 然后,在 addVersion 函数中,向 MyClass 添加了一个静态属性 version;

  • 接着,返回修改后的 MyClass 类,并将其赋值给 MyClass 变量;

  • 最后,通过调用 MyClass.version 来输出 version 属性的值,即输出 1.0。

因此,代码 MyClass.version 的执行结果是:输出 1.0。

常见的装饰器应用场景

下面是一些常见的使用装饰器的场景:

路由请求方法装饰器

function routeMethod(method) {
  // 返回一个函数,接收目标类、属性名和属性描述符作为参数
  return function(target, key, descriptor) {
    // 创建目标类的 routes 属性(如果不存在)
    target.routes = target.routes || {};
    // 将当前方法的请求方法(method)添加到目标类的 routes 属性中
    target.routes[key] = method;
    // 返回属性描述符
    return descriptor;
  };
}

class UserController {
  // 使用装饰器语法 @routeMethod('GET'),将 getUser 方法的请求方法设置为 GET
  @routeMethod('GET')
  getUser(id) {
    // ...
  }
  // 使用装饰器语法 @routeMethod('DELETE'),将 deleteUser 方法的请求方法设置为 DELETE
  @routeMethod('DELETE')
  deleteUser(id) {
    // ...
  }
}

console.log(UserController.routes);
// 输出:{getUser: "GET", deleteUser: "DELETE"}

在这个示例中,使用装饰器函数 @routeMethod 来为 UserController 类中的 getUser 和 deleteUser 方法添加请求方法。装饰器函数 routeMethod 接收一个请求方法作为参数,返回一个装饰器函数,这个装饰器函数会在目标方法上添加请求方法。在 UserController 类中,分别使用装饰器语法 @routeMethod('GET') 和 @routeMethod('DELETE') 来声明 getUser 和 deleteUser 方法,将它们对应的请求方法设置为 GET 和 DELETE。最后,通过输出 UserController 类的 routes 属性,来查看每个方法对应的请求方法。

因此,这个例子演示了如何使用装饰器函数来为类中的方法添加额外的功能,同时保持代码的简洁和易于维护。

单例模式装饰器

单例模式是一种常见的设计模式,它可以确保一个类只有一个实例,并提供全局访问点来访问该实例。在 JavaScript 中,可以使用装饰器模式来实现单例模式。

function singleton(cls) {
  // 定义一个 instance 变量,用于保存类的实例
  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 接收一个类作为参数,并返回一个新的函数。这个函数首先定义一个 instance 变量,用于保存类的实例。然后返回一个新的匿名函数,在每次调用该函数时,检查实例是否存在,如果不存在,则创建新的实例并将其保存到 instance 变量中,然后返回该实例。通过这种方式,可以确保一个类只有一个实例。

在定义 MyClass 类时,使用装饰器语法 @singleton 来调用函数 singleton,这样 MyClass 类就被装饰成了一个单例模式。最后,通过创建两个 MyClass 类的实例 a 和 b,并输出它们的比较结果,来验证单例模式的实现。

因此,这个示例演示了如何使用装饰器模式实现单例模式,装饰器函数在定义类时对其进行了修饰,使得这个类只能创建一个实例,并且可以在全局范围内访问这个实例。

自动绑定 this 装饰器

在 JavaScript 中,由于 this 的指向问题,很多时候需要手动绑定函数的 this 值,以确保函数内部的 this 引用的是预期的对象。为了避免重复地编写 this 绑定代码,可以使用装饰器模式实现自动绑定 this 功能。

function autobind(_, _2, descriptor) {
  // 从属性描述符中获取原方法 fn、configurable、enumerable
  const { value: fn, configurable, enumerable } = descriptor;
  // 返回新的属性描述符对象
  return {
    configurable,
    enumerable,
    // 定义 getter 函数来绑定 this 值并返回绑定后的方法
    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 装饰器来自动绑定 this 值
  @autobind
  handleClick() {
    console.log(this.props);
  }
}

函数 autobind 是一个装饰器函数,用于实现自动绑定 this 功能。它接收三个参数,其中 descriptor 参数表示被装饰的方法的属性描述符。在函数体内部,首先从属性描述符中获取原方法 fn、configurable 和 enumerable 三个属性。然后,返回一个新的属性描述符对象,其中包含一个 getter 函数,该函数用于绑定 this 值并返回绑定后的方法。

在 MyComponent 类中,使用装饰器语法 @autobind 来调用函数 autobind,从而实现 handleClick 方法的自动绑定 this 功能。使用 @autobind 装饰器将 handleClick 方法转换为一个 getter 函数,并在这个函数中将原方法绑定到当前实例对象上,从而实现自动绑定 this 功能。

因此,这个示例演示了如何使用装饰器模式实现自动绑定 this 功能,这种方法可以减少代码中的冗余代码,提高代码的可读性和可维护性。

日志记录

装饰器可以用于记录日志,包括打印函数调用信息、函数执行时间等信息。

日志记录是一种常见的开发需求,可以帮助开发者更好地了解程序的运行情况,并发现和解决潜在的问题。在 JavaScript 中,可以使用装饰器模式来实现日志记录功能。

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 装饰器来记录日志
    @log
    myMethod(arg1, arg2) {
      return arg1 + arg2;
    }
  }

  const obj = new MyClass();
  obj.myMethod(1, 2); // 输出:
  // Function myMethod called with 1,2
  // Function myMethod completed in 0.013614237010165215ms

函数 log 是一个装饰器函数,用于实现日志记录功能。它接收三个参数,其中 target 表示被装饰的方法所属的类的原型对象,name 表示被装饰的方法的名称,descriptor 表示被装饰的方法的属性描述符。在函数体内部,首先保存原方法的引用,然后重新定义方法,并在这个新方法中记录函数调用信息和参数、执行时间等信息,并返回原方法的执行结果。

在 MyClass 类中,使用装饰器语法 @log 来调用函数 log,从而实现 myMethod 方法的日志记录功能。使用 @log 装饰器将 myMethod 方法转换为一个新方法,并在这个新方法中记录函数调用信息和参数、执行时间等信息,从而实现日志记录功能。

因此,这个示例演示了如何使用装饰器模式实现日志记录功能,通过在装饰器函数中重新定义方法,将日志记录和函数调用分离,从而提高了代码的可读性和可维护性。

认证授权

装饰器还可以用于检查用户的认证状态和权限,以防止未经授权的用户访问敏感数据或执行操作。

在 Web 应用程序中,认证和授权是非常重要的安全机制,它们用于验证用户的身份,并授予用户访问资源和执行操作的权限。在 JavaScript 中,可以使用装饰器模式来实现认证和授权功能。

以下是一个使用装饰器模式实现认证和授权功能的示例代码:

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() {
      // 执行身份验证检查
      return true;
    }
    // 实现授权功能
    hasAccessTo(endpoint) {
      // 执行授权检查
      return true;
    }
    // 使用 @authorization 装饰器来进行身份验证和授权
    @authorization
    getUsers() {
      // 返回用户数据
    }
    // 使用 @authorization 装饰器来进行身份验证和授权
    @authorization
    deleteUser(id) {
      // 删除指定 ID 的用户
    }
  }

函数 authorization 是一个装饰器函数,用于实现身份验证和授权功能。它接收三个参数,其中 target 表示被装饰的方法所属的类的原型对象,name 表示被装饰的方法的名称,descriptor 表示被装饰的方法的属性描述符。在函数体内部,首先保存原方法的引用,然后重新定义方法,并在这个新方法中检查用户的认证状态和访问权限,并在用户未经授权或认证时返回错误信息。

在 MyApi 类中,使用装饰器语法 @authorization 来调用函数 authorization,从而实现 getUsers 和 deleteUser 方法的身份验证和授权功能。使用 @authorization 装饰器将 getUsers 和 deleteUser 方法转换为一个新方法,并在这个新方法中检查用户的身份认证状态和访问权限,从而提高应用程序的安全性。

因此,这个示例演示了如何使用装饰器模式实现身份验证和授权功能,通过在装饰器函数中重新定义方法,将身份验证和授权逻辑与方法的实现分离,从而提高了代码的可读性和可维护性。

缓存

装饰器也可以用于缓存函数的执行结果,以避免重复计算。具体来说,可以使用一个装饰器函数来将函数的输入参数作为键,将函数的执行结果作为值,将它们存储在一个对象中。在函数被调用时,首先检查缓存中是否存在对应的结果,如果有则直接返回结果,否则执行函数并将结果存入缓存。

以下是一个使用装饰器实现缓存功能的示例代码:

// 定义一个装饰器函数,实现函数缓存
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)); // 输出:
// calculate called
// cache miss: 10
// 20
console.log(math.calculate(10)); // 输出:
// cache hit: 10
// 20

在这段代码中,我们首先定义了 memoize 装饰器函数,它接收三个参数 target、name 和 descriptor,分别表示目标类、目标方法名和方法的属性描述符。在 memoize 函数内部,我们保存了原始方法 originalMethod,并创建了一个 Map 类型的缓存 cache。

接下来,我们将原始方法替换为一个新的函数。这个新函数首先将输入参数 args 转换为一个字符串类型的键 cacheKey,并检查这个键是否存在于缓存 cache 中。如果存在,则直接返回缓存中对应的结果,否则调用原始方法 originalMethod,并将结果保存到缓存 cache 中。

最后,我们将修改后的属性描述符 descriptor 返回。在 MyMath 类中的 calculate 方法上使用了 memoize 装饰器,从而实现了计算结果缓存的功能。在调用 calculate 方法时,如果参数相同,则直接返回缓存中的结果,避免了重复计算。

面向切面编程

Aspect-oriented programming (AOP) 是一种编程思想,它将程序的功能分为多个不同的方面(facets),并在运行时通过添加新的功能来实现这些方面,而无需修改原始代码。

Decorators 可以用来实现 AOP,通过在不改变原始代码的情况下,动态添加功能。例如,可以使用装饰器为某个函数添加日志记录、性能统计等功能,而无需修改原始函数的实现。这使得 AOP 成为一种灵活且可扩展的编程方式,可以帮助开发人员更好地管理和维护程序的复杂性。

// 定义装饰器 validate,接收被装饰的方法的 target、name、descriptor 三个参数
function validate(target, name, descriptor) {
  const originalMethod = descriptor.value;

  // 修改 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);
    };

  // 返回修改后的 descriptor 对象
  return descriptor;
}

// 定义 MyForm 类
class MyForm {
  // 在 submit 方法上使用 @validate 装饰器,表示该方法需要被验证参数
  @validate
  submit(name, email, message) {
    // 提交表单的逻辑
  }
}

// 创建 MyForm 的实例 form,并调用 submit 方法,但是传入的第一个参数是空字符串,不符合验证规则
const form = new MyForm();
form.submit('', '[email protected]', 'Hello world');

// Output: Invalid arguments

这段代码展示了如何使用装饰器来实现 AOP(Aspect-oriented Programming,面向切面编程)。AOP 是一种编程范式,它允许在不修改原始代码的情况下添加功能。

在本例中,我们定义了一个装饰器函数 validate,它接收三个参数:被装饰的方法的 target、name、descriptor。装饰器函数会修改被装饰的方法,以实现参数验证的功能。

在 validate 函数内部,我们首先获取被装饰的方法的原始实现(即 descriptor.value),然后定义一个新的函数来代替原始实现。新函数会首先验证所有参数是否为非空字符串,如果有任何一个参数不符合要求,就输出错误信息并返回。

然后,我们将新函数作为新的方法实现赋值给 descriptor.value 属性,并将修改后的 descriptor 对象返回。

在 MyForm 类中,我们使用 @validate 装饰器来装饰 submit 方法。这表示该方法需要被验证参数,以保证传入的参数符合要求。

最后,我们创建了 MyForm 类的实例 form,并调用了 submit 方法。由于第一个参数是空字符串,不符合验证规则,所以会输出错误信息:"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)); // 输出:3
console.log(math.calculate.reversed(1, 2)); // 输出:3

该段代码实现了一个可逆的装饰器函数,将其应用在calculate方法上,该方法接收两个参数并返回它们的和。在应用装饰器后,调用该方法时,会先将传入的参数倒序排列,再计算它们的和。此外,还可以通过在calculate方法后添加.reversed来调用不带装饰器的原始方法。

自动类型检查

装饰器可以用于自动类型检查,例如,您可以添加一个装饰器来确保函数参数的类型是正确的。这对于保证代码的正确性和避免出错非常有帮助。初学者可以通过装饰器来学习如何在代码中实现类型检查,避免由于类型不匹配导致的运行时错误。

// 定义一个装饰器函数,用于类型检查
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;
  }
}

// 创建 MyMath 实例,并使用装饰器增加的类型检查功能
const math = new MyMath();
math.add(1, '2'); // 输出:Invalid arguments: 2

这段代码是一个用于自动类型检查的装饰器,通过给方法添加装饰器实现检查传入参数的类型是否正确。该装饰器返回一个函数,该函数再返回一个装饰器。当方法被调用时,装饰器会检查传入参数的类型是否为预期类型,如果不是,会输出一个错误信息。

在这个例子中,@checkType('number') 被应用于 add 方法上,所以当 add 方法被调用时,该装饰器会检查传入的两个参数是否都是数字类型,如果不是则会输出错误信息。

结束

装饰器还有很多其他的用法,比如错误处理、数据收集、重试等。通过使用装饰器,我们可以让我们的代码更加干净、简洁和易于维护。同时,装饰器还有助于将不同的功能模块化,使得代码结构更加清晰和易于扩展。

虽然装饰器已经被正式加入到 ECMAScript 标准中,但是并不是所有的浏览器都支持。因此,在使用装饰器时,我们需要使用 Babel 等工具将装饰器转换为常规的 JavaScript 代码,以确保我们的代码可以在各种环境下正常运行。

总之,装饰器是一种非常强大且灵活的语言特性,可以大大简化我们的代码,同时还可以提高代码的可读性和可维护性。无论是在 Web 开发、桌面应用程序还是移动应用程序开发中,装饰器都是非常有用的工具,值得我们深入学习和掌握。

今天的分享就到这里,感谢你的阅读,希望能够帮助到你,文章创作不易,如果你喜欢我的分享,别忘了点赞转发,让更多有需要的人看到,最后别忘记关注「前端达人」,你的支持将是我分享最大的动力,后续我会持续输出更多内容,敬请期待。

猜你喜欢

转载自blog.csdn.net/Ed7zgeE9X/article/details/129891163