Декораторы JavaScript с нуля

Spread the love

Перевод: Mahdhi RezviJavaScript Decorators From Scratch

Что такое декоратор?

Декоратор — это просто способ обернуть функцию другой функцией, чтобы расширить ее существующие возможности. Вы «декорируете» существующий код, оборачивая его другим фрагментом кода. Эта концепция не является новой для тех, кто знаком с функциональной композицией или функциями высшего порядка.

В декораторах нет ничего нового. Они широко распространены в других языках, таких как Python, и даже в JavaScript в рамках функционального программирования. Но об этом мы поговорим позже.

Зачем использовать декоратор?

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

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

Внедрение отдельного синтаксиса декораторов для классов в JavaScript находиться на этапе 2 предложения (stage 2 proposal). Об это чуть ниже.

Декораторы функций

Что такое декораторы функций?

Декораторы функций — это просто функции. Они получают функцию в качестве аргумента и возвращают другую функцию, которая улучшает и расширяет функцию аргумент. Новая функция не изменяет функцию аргумент, а скорее использует ее в своем теле. Это очень похоже на функции высшего порядка, как я упоминал ранее.

Как работает декоратор функций?

Давайте разберемся с декораторами функций на примере.

Рассмотрим пример проверки аргументов. В таких языках, как Java, если ваша функция ожидает, что будут переданы два аргумента а вы передадите три аргумента, вы получите исключение. Но с JavaScript вы не получите никаких ошибок, так как дополнительные параметры просто игнорируются. Такое поведение может быть раздражающим или полезным.

Чтобы убедиться, что аргументы, передаваемые функции, действительны, мы можем проверить их при входе. Это простой процесс, в котором вы проверяете каждый параметр на предмет требуемого типа данных и убеждаетесь, что количество аргументов не превышает требуемого количества параметров.

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

//decorator function
const allArgsValid = function(fn) {
  return function(...args) {
  if (args.length != fn.length) {
      throw new Error('Only submit required number of params');
    }
    const validArgs = args.filter(arg => Number.isInteger(arg));
    if (validArgs.length < fn.length) {
      throw new TypeError('Argument cannot be a non-integer');
    }
    return fn(...args);
  }
}

//ordinary multiply function
let multiply = function(a,b){
	return a*b;
}

//decorated multiply function that only accepts the required number of params and only integers
multiply = allArgsValid(multiply);

multiply(6, 8);
//48

multiply(6, 8, 7);
//Error: Only submit required number of params

multiply(3, null);
//TypeError: Argument cannot be a non-integer

multiply('',4);
//TypeError: Argument cannot be a non-integer

allArgsValid — это функция-декоратор, которая принимает функцию в качестве аргумента. Эта функция-декоратор возвращает другую функцию, которая обертывает аргумент функции. Более того, она вызывает функцию аргумента только тогда, когда аргументы, передаваемые в функцию аргумента, являются действительными целыми числами. В противном случае выдается ошибка. Она также проверяет количество передаваемых параметров и следит за тем, чтобы оно не превышало или не искажало требуемое количество параметров.

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

//ordinary add function
let add = function(a,b){
	return a+b;
}

//decorated add function that only accepts the required number of params and only integers
add = allArgsValid(add);

add(6, 8);
//14

add(3, null);
//TypeError: Argument cannot be a non-integer

add('',4);
//TypeError: Argument cannot be a non-integer

Предложение декоратора классов TC39

Декораторы функций давно существуют в JavaScript в рамках функционального программирования. Но декораторы классов все еще находятся на этапе внедрения.

Классы JavaScript на самом деле не классы. Это просто синтаксический сахар, который упрощает работу разработчиков.

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

Давайте посмотрим на пример того, как это можно реализовать.

function log(fn) {
  return function() {
    console.log("Execution of " + fn.name);
    console.time("fn");
    let val = fn();
    console.timeEnd("fn");
    return val;
  }
}

class Book {
  constructor(name, ISBN) {
    this.name = name;
    this.ISBN = ISBN;
  }

  getBook() {
    return `[${this.name}][${this.ISBN}]`;
  }
}

let obj = new Book("HP", "1245-533552");
let getBook = log(obj.getBook);
console.log(getBook());
//TypeError: Cannot read property 'name' of undefined

Причина ошибки заключается в том, что при вызове метода getBook он фактически вызывает анонимную функцию, возвращаемую функцией декоратора log. Внутри этой анонимной функции вызывается метод obj.getBook. Но значение this в анонимной функции относится к глобальному объекту, а не к объекту Book. Следовательно, мы получаем ошибку типа.

Мы можем исправить эту проблему, передав экземпляр объекта book методу getBook.

function log(classObj, fn) {
  return function() {
    console.log("Execution of " + fn.name);
    console.time("fn");
    let val = fn.call(classObj);
    console.timeEnd("fn");
    return val;
  }
}

class Book {
  constructor(name, ISBN) {
    this.name = name;
    this.ISBN = ISBN;
  }

  getBook() {
    return `[${this.name}][${this.ISBN}]`;
  }
}

let obj = new Book("HP", "1245-533552");
let getBook = log(obj, obj.getBook);
console.log(getBook());
//[HP][1245-533552]

Мы также должны передать bookObj в функцию декоратора log, чтобы иметь возможность передать его как this в метод obj.getBook.

Хотя это решение работает отлично, это своего рода обходной путь. В новом стандарте синтаксис стал более оптимизированным, что упрощает реализацию этих решений.

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

Декораторы согласно разрабатываемому стандарту используют специальный синтаксис, в котором они начинаются с символа @. Наша функция декоратора log будет вызываться с использованием синтаксиса, как показанно ниже.

@log

В этом стандарте есть некоторые изменения в функции декоратора. Когда функция декоратора применяется к классу, функция декоратора получит только один аргумент. Этот аргумент называется target, которая в основном является объектом декорируемого класса.

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

Давайте посмотрим на пример с использованием класса Book, который мы использовали ранее.

function log(target) {
  return function(...args) {
    console.log("Constructor called");
    return new target(...args);
  };
}

@log
class Book {
  constructor(name, ISBN) {
    this.name = name;
    this.ISBN = ISBN;
  }

  getBook() {
    return `[${this.name}][${this.ISBN}]`;
  }
}

let obj = new Book("HP", "1245-533552");
//Constructor Called
console.log(obj.getBook());
//HP][1245-533552]

Как вы можете видеть выше, декоратор log получает аргумент target и возвращает анонимную функцию, которая выполняет оператор log и создает и возвращает новый экземпляр target, который в основном является классом Book. Вы также можете добавить прототипы к target, используя target.prototype.property.

Кроме того, в классе можно использовать несколько функций-декораторов, как показано ниже.

function logWithParams(...params) {
  return function(target) {
    return function(...args) {
      console.table(params);
      return new target(...args);
    }
  }
}

@log
@logWithParams('param1', 'param2')
class Book {
	//Class implementation as before
}

let obj = new Book("HP", "1245-533552");
//Constructor called
//Params will be consoled as a table
console.log(obj.getBook());
//[HP][1245-533552]

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

Подобно декоратору класса, декораторы свойств класса также используются с аналогичным синтаксисом. Функция декоратора начинается с символа «@». Вы также можете передать параметры функции декоратора со свойствами класса, как показано в приведенных выше примерах.

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

Аргументы, передаваемые в декоратор метода класса, будут отличаться от декоратора класса. Декоратор метода класса получит три параметра вместо одного. Они следующие.

  • Target — target относится к объекту, который содержит конструктор и методы внутри класса.
  • Name — name относится к имени метода, для которого вызывается декоратор.
  • Descriptor — дескриптор относится к объекту дескриптора вызываемого метода. Вы можете узнать больше о дескрипторах свойств здесь.

Descriptor аргумент дескриптора, которым большую часть времени будут манипулировать для выполнения требования. Объект дескриптора имеет 4 атрибута при использовании в методе класса. Они следующие.

  • Configurable — логический атрибут, который определяет, можно ли изменять дескрипторы свойств
  • Enumerable — логический атрибут, который определяет, будет ли свойство отображаться во время перечисления объекта
  • Value — это является значением свойства. В нашем случае это функция.
  • Writable —логический атрибут, определяющий возможность перезаписи свойства.

Давайте посмотрим на пример с нашим классом Book.

//readonly decorator function
function readOnly(target, name, descriptor) {
  descriptor.writable = false;
  return descriptor;
}

class Book {
  //Implementation here
  @readOnly
  getBook() {
    return `[${this.name}][${this.ISBN}]`;
  }

}

let obj = new Book("HP", "1245-533552");

obj.getBook = "Hello";

console.log(obj.getBook());
//[HP][1245-533552]

В приведенном выше примере используется функция декоратора readOnly, которая делает метод getBook в классе Book доступным только для чтения. Это достигается установкой для свойства дескриптора writable значения false. По умолчанию для этого свойства установлено значение true.

Если свойство, writable , не изменять, то вы можете легко перезаписать свойство getBook, как показано ниже.

obj.getBook = "Hello";

console.log(obj.getBook);
//Hello

Декораторы поля класса

Подобно методам класса, поля класса также могут быть декорированы. Хотя поля классов поддерживаются в typescript, они все еще находятся на стадии 3 предложения для JavaScript.

Аргументы, передаваемые функции декоратора при использовании в полях класса, такие же, как аргументы, передаваемые при использовании в методах класса. Единственная разница заключается в объекте дескриптора. В отличие от метода класса, объект дескриптора не будет содержать атрибут value при использовании в поле класса. Этот атрибут будет заменен атрибутом, называемым initializer, который является функцией. Поскольку стандарт декораторов полей классов все еще находятся в стадии предложения, вы можете узнать больше о функции initializer в документации. Функция initializer возвращает начальное значение переменной полей классов.

Более того, атрибут объекта дескриптора writable  не будет присутствовать, если значение поля не определено.

Давайте рассмотрим пример. Мы снова будем использовать наш класс Book.

function upperCase(target, name, descriptor) {
  if (descriptor.initializer && descriptor.initializer()) {
    let val = descriptor.initializer();
    descriptor.initializer = function() {
      return val.toUpperCase();
    }
  }

}

class Book {
  
  @upperCase
  id = "az092b";

  getId() {
    return `${this.id}`;
  }

  //other implementation here
}

let obj = new Book("HP", "1245-533552");

console.log(obj.getId());
//AZ092B

В приведенном выше примере значение свойства id преобразуется в верхний регистр. Мы использовали функцию декоратора под названием upperCase, которая проверяет наличие initializer, чтобы убедиться, что значение не является неопределенным, проверели, является ли значение истинным, а затем преобразовали его в верхний регистр. Когда вызывается метод getId, можно увидеть значение верхнего регистра. Подобно другим функциям декоратора, вы можете передавать параметры, когда декораторы также используются в полях класса.

Сценарии использования

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

Декораторы в Angular

Если кто-нибудь знаком с typescript и Angular, он наверняка сталкивался с декораторами, используемыми в классах Angular. Вы можете найти такие декораторы, как «@Component», «@NgModule», «@Injectable», «@Pipe» и другие. Эти декораторы загружаются встроенными и декорируют классы.

MobX

MobX активно использовал и поощрял декораторы до версии 6. В качестве декораторов использовались «@observable», «@computed» и «@action». Но MobX в настоящее время не поощряет использование декораторов, поскольку это предложение еще не стандартизировано. В документации указано следующее:

Однако декораторы в настоящее время не являются стандартом ES, и процесс стандартизации занимает много времени. Также похоже, что стандарт будет отличаться от того, как декораторы были реализованы ранее.

Декораторы в Core Decorators

Эта библиотека JavaScript предоставляет готовые декораторы из коробки. Хотя эта библиотека основана на предложении декоратора стадии 0, автор библиотеки ожидает, пока предложение достигнет стадии 3, чтобы обновить библиотеку.

Эта библиотека поставляется с такими декораторами, как «@readonly», «@time», «@deprecate» и другими. Вы можете узнать больше здесь.

Библиотека Redux в React

Библиотека Redux для React содержит метод подключения, который позволяет вам подключить компонент React к хранилищу Redux. Библиотека позволяет также использовать метод connect в качестве декоратора.

//Before decorator
class MyApp extends React.Component {
  // ...define your main app here
}
export default connect(mapStateToProps, mapDispatchToProps)(MyApp);//After decorator
@connect(mapStateToProps, mapDispatchToProps)
export default class MyApp extends React.Component {
  // ...define your main app here
}

Ответ Феликса Клинга на Stack Overflow разъясняет этот пример.

Более того, хотя connect поддерживает синтаксис декоратора, команда redux в настоящее время не одобряет его. В основном это связано с тем, что предложение декоратора находится на этапе 2 и может изменена в будущем.

Декораторы — это мощный инструмент, позволяющий создавать очень гибкий код. Очень высока вероятность того, что вы будете часто встречаться с ними в ближайшем будущем.

Спасибо за чтение и happy coding

Была ли вам полезна эта статья?
[8 / 3.8]

Spread the love
Подписаться
Уведомление о
guest
0 Комментарий
Oldest
Newest Most Voted
Inline Feedbacks
View all comments