前端提效新特性-javascript装饰器

112 阅读4分钟

在本文中,我们将了解装饰器及其实现的两种不同技术 - 函数式编程和类编程。 大多数流行的语言例如java和Python都是有装饰器的,这是一种常见的模式,主要有以下两种目的:

  1. 在运行时更改对象的功能而不影响对象的现有功能
  2. 将行为包装成简单的、可重用的块,同时可以减少代码数量

可以在没有装饰器的情况下进行编码,因为它们主要是语法糖,允许我们包装和注释类和函数,但它们极大地提高了代码的可读性。

JavaScript中的装饰器

从技术上讲,你可以将 JavaScript 中的装饰器视为一种高阶函数,它将一个函数作为参数并返回另一个函数作为结果。在大多数实现中,返回的函数重用它作为参数的原始函数,但这不是必需的。JS标准化进程进展缓慢,但已经取得了进展,因此我们很可能在不久的将来看到装饰器成为该语言的一部分。同时,TypeScript 和 Babel 已经可以转译和填充装饰器,因此我们可以在日常开发中使用它们。事实上,装饰器在各种前端 JavaScript 框架中变得越来越流行:Angular、Mobx 和 Vue.js。

JavaScript 提案定义了四种类型的装饰器:

  • 类装饰器
  • 属性装饰器
  • 方法装饰器
  • 访问器装饰器

在本文中,我们将回顾应用于基于类的实现中的方法以及应用于函数(如果使用函数式方法)的装饰器。

装饰器实现

让我们创建一个类Notification,它由属性type和一个简单的方法notifyUser组成,该方法在控制台会去打印通知的类型。之后,我们将创建一个Notification类的实例并调用notifyUser方法:

class Notification {
  type: string;
  
  constructor(){
    this.type = 'Success';
  }
  
  notifyUser = function(){
    console.log(`${this.type} notification`);
  }
}

const notification = new Notification('Success');
notification.notifyUser();

现在我们需要在一段延迟后(例如 3 秒)在控制台中显示此通知消息。

为了实现这个新要求,我们可以引入一个新类 DelayedNotification,但它几乎与 Notification 完全相同.感觉我们只需要稍微修改现有Notification类的行为即可。这是装饰器的实现完美契合的情况。

装饰器作为高阶函数

首先,我们需要用 setTimeout API 来修饰现有的方法 notifyUser。然后我们需要以某种方式向装饰器传递毫秒数。让我们实现一个通用的高阶函数delayMiliseconds,它将接受任何其他函数并通过在调用它时添加延迟来装饰它。

const delayMiliseconds = (fn: Function, delay: number = 0) => () => {
  setTimeout(() => fn(), delay);
  return 'notifyUser is called';
}

让我们看看如何调用装饰器:

delayMiliseconds(notification.notifyUser, 3000);

现在,要将这个通用函数应用于类,我们可以在方法 notificationUser 中进行以下更改:

class DelayedNotification {
  type: string;
  
  constructor(type){
    this.type = type;
  }
  
  public notifyUser = delayMiliseconds(() => {
    console.log(`${this.type} notification` 'checkTime:' new Date().getSeconds());
  },3000);
}

const notification = new DelayedNotification('Success');

console.log(notification.notifyUser() 'checkTime:' new Date().getSeconds())

结果,3秒后我们将在控制台中看到“成功通知”消息:

image.png

从这个例子中,我们可以看到delayMiliseconds是一个高阶函数——一个接受一个函数作为参数并返回另一个函数的函数。如果你仔细观察数组、字符串、DOM 方法、promise 方法上的 JavaScript 函数,你会发现它们中的许多都是高阶函数,因为它们接受函数作为参数。

函数式方法通常不使用类,因此工厂模式是在 JS 中创建对象的常见方法。我们上面创建的高阶函数也可以在这里恰当地使用,如下所示:

function functionBasedNotificationFactory(){
  const type = 'Success';
  
  notifyUser(){
    console.log(`${type} notification`);
  }
  
  return {
    name: 'Success',
    notifyUser: delayMiliseconds(notifyUser, 300);
  }
}

const notification = functionBaseNotification();
notifycation.notifyUser();

从上面的内容中可以看到,工厂函数functionBasedNotificationFactory返回带有装饰方法notifyUser的对象。

基于类的方法

让我们看一下装饰器模式作为语言特性的实现。基于类的实现:

function delayMiliseconds(milliseconds: number = 0){
  return function(target: Object, propertyKey: string | symbol,descriptor: PropertyDescriptor){
   const originalMethod = descriptor.value;
   descriptor.value = function(...args){
     setTimeout(() => {
       originalMethod.call(this, ...args);
     }, milliseconds);
   };
   return descriptor;
  };
}

装饰器delayMiliseconds采用一个参数毫秒(毫秒的默认值为0)。该参数将在 setTimeout 函数中用作延迟量.

正如在上面看到的,delayMiliseconds 采用三个参数:

  • target - 静态成员的类的构造函数或实例成员的类的原型。在我们的示例中,Notification.prototype 将是一个目标。
  • propertyKey - 正在修饰的方法名称。
  • PropertyDescriptor - 描述 [Object] 的属性:
interface PropertyDescriptor {
    configurable?: boolean;
    enumerable?: boolean;
    value?: any;
    writable?: boolean;
    get? (): any;
    set? (v: any): void;
}

如果我们想改变方法的行为,我们应该使用新功能重新定义属性描述符中的值

const originalMethod = descriptor.value; // a reference to the original

descriptor.value = function (...args) {
  setTimeout(() => {
	 originalMethod.call(this, ...args); // bind a context of Notifica
  }, milliseconds); 
};

return descriptor;   // return descriptor with a new behaviour

要将装饰器应用到我们的方法notifyUser,我们应该使用特殊的语法 - 前缀符号@。

class DelayedNotification {
	type: string; 

	constructor(type) {
		this.type = type
	}

 	@delayMiliseconds(300)
 	notifyUser() {
		console.log(`${this.type} notification`);
	}
}

让我们在控制台中显示一下 DelayedNotification 的实例:

image.png

从这个截图中,我们可以观察到DelayedNotification实例没有notifyUser方法。 NotifyUser方法仅存在于DelayedNotification原型中。发生这种情况是因为delayMiliseconds 应该与原型方法一起使用,因为它依赖于描述符。

还有额外的证据证明这一点 - 让我们在控制台中运行相同的验证,正如我们之前在功能方法中提供的那样:

image.png 结果,您在控制台中看到 true ,就像在函数方法中一样,因为通知原型有一个方法 notifyUser。

正如您所看到的,函数式方法和基于类的方法之间的实质性区别之一是,我们在基于类的方法中修饰了原型上的方法,而在函数式方法中则没有。 带有类的工厂模式不像函数式方法那么流行,因为您可以通过其类直接实例化对象。但如果我们想这样做,我们不需要做任何特别的事情。当 JS 创建类实例时,装饰器将自动应用。让我们创建一个基于类的工厂:

function classBasedObjectAFactory() {
  return new Notification('Success');
}

const notification = classBasedObjectAFactory();

notification.notifyUser();

// 'Success notification' in console after 3 seconds

从上面的内容中可以看出,工厂函数提供了通过使用 new 关键字创建类实例来生成对象的能力。

总结

装饰器是一个能够动态修改现有功能的高阶函数,使用装饰器的主要目的是在不修改原有功能的情况下动态地向对象添加逻辑

您可以在以下情况下使用装饰器:

  • 您想要向该对象添加新功能(稍后可以轻松地从此对象中删除该功能)
  • 通过创建装饰器来替换类的继承