Создание и использование JavaScript-декораторов

Декораторы – это функции, которые изменяют другие функции. Декораторы могут быть полезны для применения одинаковых изменений к нескольким функциям.

В этой статье мы рассмотрим, как создавать декораторы в JavaScript и использовать их для изменения методов и классов.

Декораторы методов

Декораторы методов JavaScript – это функции с несколькими параметрами: target, property и descriptor.

Target – обозначает класс, в котором находится модифицируемый метод, property – это строка с названием свойства (или метода), подлежащего изменениям, а descriptor – содержит несколько свойств для описания изменений.

Декоратор может использоваться только в классах, хотя класс JavaScript является просто синтаксическим сахаром над функциями конструктора.

Это относительно новая конструкция, которая отдельными браузерами может не поддерживаться, поэтому, чтобы заставить их работать, может понадобиться Babel или TypeScript.

Например, напишем класс Person и используем декоратор readonly, чтобы метод description этого класса стал доступным только для чтения:

const readonly = (target, property, descriptor) => {
  descriptor.writable = false
  return descriptor
}
class Person {
  constructor(name, age) {
    this.name = name
    this.age = age
  }
  @readonly
  description() {
    return `${this.name} is ${this.age}`
  }
}

Здесь в target будет класс Person. В Person есть конструктор и напишем в этом классе метод description.

Property – это строка с именем участника класса (свойство или метод), который будет модифицирован. В приведенном выше коде, это будет метод description класса Person, поэтому property содержит строку 'description'.

У descriptor есть несколько свойств для описания содержимого декоратора:

  • value – здесь будет значение свойства
  • writable – булёвое выражение, влияет на возможность изменения значения (можно ли перезаписывать)
  • enumerable – булёвое выражение, может ли значение перебираться в цикле
  • configurable – булёвое выражение, обозначает возможность вносить в свойство изменения

В функции декоратора readonly установим значение false для descriptor.writable, поэтому после применения декоратора readonly к методу description его значение уже будет невозможно изменить.

Это легко проверить:

let person = new Person('Joe', 10);
console.log(person.description);
person.description = 'foo';
console.log(person.description);

Прежде чем установить значение foo для person.description, получим:

ƒ description() {
    return `${this.name} is ${this.age}`;
  }

Затем попробуем установить значение foo для person.description, но оно не изменится и останется прежним:

ƒ description() {
    return `${this.name} is ${this.age}`;
  }

Посмотрите пример, он подтверждает, что декоратор readonly не позволил перезаписать содержимое метода description.

See this code readonly decorator on x.xhtml.ru.

Декораторы классов

Декораторы классов определяются перед описанием класса. Они используются для изменения класса или функции конструктора. Следовательно, они должны возвращать функцию конструктора или новый класс.

Декораторы классов принимают только один параметр – класс, перед которым они определены.

Например, использовать его можно следующим образом:

const addGender = (PersonClass) => {
  PersonClass = class Person {
    constructor(name, gender) {
      this.name = name;
      this.gender = gender;
    }
  }
  return PersonClass;
}
@addGender
class Person {
  constructor(name) {
    this.name = name;
  }
}
let person = new Person('Joe', 'male');
console.log(person.gender);

Здесь addGender – это декоратор класса Person.

В функции декоратора создаётся и возвращается новый класс, в котором к существующему свойству name добавляем новое свойство gender.Теперь его можно использовать следующим образом:

let person = new Person('Joe', 'male');
console.log(person.gender);

Как видите, person.gender теперь можно прочитать и его значением будет 'male'.

Это означает, что person теперь возвращается экземпляром класса, который вернула функция декоратора.

Ещё из декоратора можно вернуть функцию конструктора вместо класса:

const addGender = (PersonClass) => {
  PersonClass = function Person(name, gender) {
    this.name = name;
    this.gender = gender;
  }
  return PersonClass;
}
@addGender
class Person {
  constructor(name) {
    this.name = name;
  }
}

Тут будет получен аналогичный результат для person.gender, так:

let person = new Person('Joe', 'male');
console.log(person.gender);

Вот так это будет работать:

See this code class decorator on x.xhtml.ru.

Несколько декораторов

Для одного метода или класса можно использовать несколько декораторов. Их надо перечислить перед кодом метода (класса), тогда они будут последовательно выполняться и возвращать результат. Порядок выполнения функций – от ближайшего к классу до самого верхнего в списке декораторов.

const addGender = (PersonClass) => {
  console.log('1. addGender');
  PersonClass = function Person(name, gender) {
    this.name = name;
    this.gender = gender;
  }
  return PersonClass;
}

const addAge = (PersonClass) => {
  console.log('2. addAge');
  const TmpPersonClass = PersonClass;
  PersonClass = function Person(name, gender, age) {
    TmpPersonClass.call(this, name, gender);
    this.age = age;
  }
  return PersonClass;
}

@addAge  // выполнится последним
@addGender // выполнится первым
class Person {
  constructor(name) {
    this.name = name;
  }
}

const person = new Person('Joe', 'male', '46');
console.log(person.name, person.gender, person.age);

В этом примере сперва выполняется ближайший к описанию класса Person декоратор @addGender, а последним – @addAge.

See this code multiple decorators on x.xhtml.ru.

Подведём итог.

Для изменения методов и классов JavaScript можно создавать декораторы.

Декоратор метода принимает в качестве параметров target, property и descriptor. Декоратор возвращает изменения для метода.

Функция декоратора класса возвращает новую функцию класса или конструктора. Декоратор принимает класс в качестве параметра, при этом нет необходимости изменять переданный класс. Можно просто возвращать новый класс с изменениями.

Для одного метода или класса можно использовать несколько декораторов, которые будут выполняться последовательно.