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

от автора

Будущих студентов курса «JavaScript Developer. Professional» приглашаем записаться на открытый урок по теме «Делаем интерактивного telegram бота на Node.js».

А сейчас делимся традиционным переводом полезного материала.


Разбираемся с функциями-декораторами

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

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

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

Зачем нужны декораторы?

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

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

Сейчас предложение к стандарту о декораторах классов находится на 2-м этапе рассмотрения, и к этому предложению еще может добавиться множество полезных дополнений.

Совет. Делитесь компонентами многоразового использования для разных проектов на платформе Bit (Github). Это простой способ документировать и систематизировать независимые компоненты из любых проектов и делиться ими.

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

Bit поддерживает Node, TypeScript, React, Vue, Angular и другие фреймворки JS.

Примеры React-компонентов многоразового использования на Bit.dev
Примеры React-компонентов многоразового использования на Bit.dev

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

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

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

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

Рассмотрим пример.

Проверка аргументов — обычная практика в программировании. В таких языках, как 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 декораторы функций используются уже давно. Предложение о декораторах классов находится на 2-м этапе рассмотрения.

Классы в 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. Возникает ошибка TypeError.

Это можно исправить, передав экземпляр объекта 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]

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

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

Примечание. Для выполнения кода из приведенных ниже примеров можно использовать Babel. JSFiddle — более простая альтернатива, которая позволяет опробовать эти примеры в браузере. Предложения еще не дошли до последнего этапа рассмотрения, поэтому использовать их в продакшене не рекомендуется: их функционирование пока не идеально и в синтаксис могут внести изменения.

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

В новых декораторах используется специальный синтаксис с префиксом @. Для вызова функции-декоратора log будем использовать такой синтаксис:

@log

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

Имея доступ к аргументу 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 — объект, в котором содержатся конструктор и методы, объявленные внутри класса;

  • 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 поддерживает поля классов, предложение добавить их в JavaScript пока находится на 3-м этапе рассмотрения.

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

Если полю значение не присвоено (undefined), атрибут 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, чтобы гарантировать, что значению поля присвоено значение (то есть значение не является undefined). Затем она проверяет, является ли присвоенное значение «условно истинным» (прим. пер.: англ. truthy — значение, превращающееся в true при приведении к типу Boolean), и затем переводит его в верхний регистр. При вызове метода 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 есть метод connect, с помощью которого можно подключить компонент 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 }

В ответе пользователя Felix Kling на Stack Overflow можно найти некоторые пояснения.

Хотя connect поддерживает синтаксис декоратора, в настоящее время команда Redux не приветствует его использование. Связано это в основном с тем, что предложение о декораторах находится на 2-м этапе рассмотрения, а значит, в него могут быть внесены изменения.

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

Спасибо, что прочитали, и чистого вам кода!


Узнать подробнее о курсе «JavaScript Developer. Professional».

Записаться на открытый урок по теме «Делаем интерактивного telegram бота на Node.js».

ссылка на оригинал статьи https://habr.com/ru/company/otus/blog/531664/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *