了解JavaScript装饰器

99 阅读5分钟

简介

根据《剑桥词典》,"装饰 "的意思是 "在一个物体或地方添加一些东西,特别是为了使它更有吸引力"。

在编程中,装饰就是简单地用另一段代码来包装,从而装饰它。装饰器(也称为装饰函数)可以另外指的是用另一个函数包装一个函数以扩展其功能的设计模式。

这个概念在JavaScript中是可能的,因为有第一类函数--被当作第一类公民的JavaScript函数。

装饰器的概念在JavaScript中并不新鲜,因为高阶函数是函数装饰器的一种形式。

让我们在下一节中详细介绍一下。

函数装饰器

函数装饰器就是函数。它们接受一个函数作为参数,并返回一个新的函数,在不修改函数参数的情况下增强该函数。

高阶函数

在JavaScript中,高阶函数将一个一级函数作为参数,并且/或者返回其他函数。

考虑一下下面的代码。

const logger = (message) => console.log(message)

function loggerDecorator (logger) {
    return function (message) {
        logger.call(this, message)
        console.log("message logged at:", new Date().toLocaleString())
    }
}

const decoratedLogger = loggerDecorator(logger);

我们通过使用loggerDecorator 函数装饰了logger 函数。返回的函数--现在存储在decoratedLogger 变量中--并没有修改logger 函数。相反,返回的函数装饰了它,使其能够打印消息记录的时间。

考虑一下下面的代码。

logger("Lawrence logged in: logger") // returns Lawrence logged in: logger

decoratedLogger("Lawrence logged in: decoratedLogger") 
// returns:
// Lawrence logged in: decoratedLogger
// message logged at: 6/20/2021, 9:18:39 PM

我们看到,当logger 函数被调用时,它将消息记录到控制台。但是当decoratedLogger 函数被调用时,它把消息和当前时间都记录到了控制台。

下面是另一个合理的函数装饰器的例子。

//ordinary multiply function
let Multiply = (...args) => {
    return args.reduce((a, b) => a * b)
}

// validated integers
const Validator = (fn) => {
  return function(...args) {
    const validArgs = args.every(arg => Number.isInteger(arg));
    if (!validArgs) {
      throw new TypeError('Argument cannot be a non-integer');
    }
    return fn(...args);
  }
}

//decorated multiply function that only multiplies integers
MultiplyValidArgs = Validator(Multiply);
MultiplyValidArgs(6, 8, 2, 10);

在我们上面的代码中,我们有一个普通的Multiply 函数,给我们所有参数的乘积。然而,通过我们的Validator 函数--它是一个装饰器--我们扩展了Multiply 函数的功能,以验证其输入并只乘以整数。

类装饰器

在JavaScript中,由于语言支持高阶函数,所以存在函数装饰器。在函数装饰器中使用的模式不容易被用于JavaScript类。因此,提出TC39类装饰器。你可以在这里了解更多关于TC39的过程

TC39类装饰器提案的目的是解决这个问题。

function log(fn) {
  return function() {
    console.log("Logged at: " + new Date().toLocaleString());
    return fn();
  }
}
class Person {
  constructor(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
  }
  getBio() {
    return `${this.name} is a ${this.age} years old ${this.job}`;
  }
}

// creates a new person
let man = new Person("Lawrence", 20, "developer");

// decorates the getBio method
let decoratedGetBio = log(man.getBio); 
decoratedGetBio(); // TypeError: Cannot read property 'name' of undefined at getBio

我们试图使用函数装饰器技术来装饰getBio 方法,但它不起作用。我们得到一个TypeError ,因为当getBio 方法在log 函数内部被调用时,this 变量将内部函数指向全局对象。

我们可以通过将this 变量绑定到Person 类的man 实例来解决这个问题,如下图所示。

// decorates the getBio method
let decoratedGetBio = log(man.getBio.bind(man));

decoratedGetBio(); // returns
// Logged at: 6/22/2021, 11:56:57 AM
// Lawrence is a 20 years old developer

虽然这个方法可行,但它需要一点黑客技术和对JavaScriptthis 变量的良好理解。因此,我们需要一种更干净、更容易理解的方法来使用类的装饰器。

类装饰器--或者说严格意义上的装饰器--是一个用于扩展JavaScript类的提案。TC39目前是一个第二阶段的提议,意味着它们有望被开发并最终包含在语言中。

然而,随着ES2015+的引入,以及转置已经成为普遍现象,我们可以在Babel等工具的帮助下使用这一功能。

装饰器使用一种特殊的语法,即在前缀中加上一个@ 符号,并紧贴在被装饰的代码上方,如下图所示。

@log
class ExampleClass {
  doSomething() {
    //
  }
}

类装饰器的类型

目前,支持的装饰器类型是在类和类的成员上--如方法、获取器和设置器。

让我们在下面进一步了解它们。

类成员装饰器

类成员装饰器是一个应用于类成员的三元函数。它有以下参数。

  • 目标 - 这指的是包含成员属性的类
  • 名称 - 这是指我们在类中装饰的成员属性的名称
  • 描述符 - 这是描述符对象,具有以下属性:值、可写、可枚举和可配置

描述符对象的value 属性指的是我们正在装饰的类的成员属性。这使得我们可以替换我们装饰的函数的模式成为可能。

让我们通过重写我们的log decorator 来了解一下。

function log(target, name, descriptor) {
  if (typeof original === 'function') {
    descriptor.value = function(...args) {
      console.log("Logged at: " + new Date().toLocaleString());
      try {
        const result = original.apply(this, args);
        return result;
      } catch (e) {
        console.log(`Error: ${e}`);
        throw e;
      }
    }
  }
  return descriptor;
}


class Person {
  constructor(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
  }

  @log
  getBio() {
    return `${this.name} is a ${this.age} years old ${this.job}`;
  }
}

// creates a new person
let man = new Person("Lawrence", 20, "developer");

man.getBio()

在上面的代码中,我们已经成功地重构了我们的log decorator - 从函数装饰器模式到成员类装饰器。

我们只是用descriptor value 来访问成员类的属性--在这种情况下,getBio 方法--然后用一个新的函数代替它。

这比普通的高阶函数更干净,更容易被重用。

类装饰器

这些装饰器被应用于整个类,使我们能够装饰类。

类装饰器函数是一个单选函数,它以被装饰的构造函数为参数。

考虑一下下面的代码。

function log(target) {
  console.log("target is:", target,);
  return (...args) => {
    console.log(args);
    return new target(...args);
  };
}


@log
class Person {
  constructor(name, profession) {
  }
}

const lawrence = new Person('Lawrence Eagles', "Developer");
console.log(lawrence);

// returns
// target is: [Function: Person]
// [ 'Lawrence Eagles', 'Developer' ]
// Person {}

在我们这个被设计的小例子中,我们记录了target 参数--构造函数--和提供的参数,然后返回一个用这些参数构造的类的实例。

为什么是装饰器?

装饰器使我们能够写出更干净的代码,因为它提供了一种有效的、可理解的方式,将一段代码与另一段代码包装起来。它还为应用这种包装器提供了一种简洁的语法。

这种语法使我们的代码不那么令人分心,因为它将增强功能的代码从核心功能中分离出来。而且它使我们能够在不增加代码复杂性的情况下增加新的功能。

此外,装饰器帮助我们将相同的功能扩展到多个函数和类中,从而使我们写的代码更容易调试和维护。

虽然装饰器已经作为高阶函数存在于JavaScript中,但在类中实现这种技术是困难的,甚至是不可能的。因此,TC39提供的特殊语法是为了方便在类中使用。

总结

尽管装饰器是一个第二阶段的提议,但它们在JavaScript世界中已经很流行了--这要感谢Angular和TypeScript

从这篇文章中,我们可以看到,它们促进了代码的可重用性,从而使我们的代码保持干燥。

在我们等待装饰器正式在JavaScript中出现的时候,你可以通过使用Babel开始使用它们。我相信你在这篇文章中已经学到了足够的知识,可以在你的下一个项目中尝试一下装饰器。

The postUnderstanding JavaScript decoratorsappeared first onLogRocket Blog.