Будущих студентов курса "JavaScript Developer. Professional" приглашаем записаться на открытый урок по теме "Делаем интерактивного telegram бота на Node.js".
А сейчас делимся традиционным переводом полезного материала.
Разбираемся с функциями-декораторами
Что такое декоратор?
Декоратор это средство, которое позволяет обернуть одну функцию другой и расширить ее возможности. Вы декорируете существующий код, обернув его другим кодом. Этот прием известен всем, кто знаком с композицией функций или функциями высшего порядка.
Декораторы явление не новое. Они используются и в других языках, например в Python, и даже в функциональном программировании на JavaScript. Но об этом мы поговорим позже.
Зачем нужны декораторы?
Они позволяют писать более чистый код, придерживаясь концепции композиции, и распространять единожды разработанную возможность на несколько функций и классов. Используя декораторы, вы сможете писать код, который проще отлаживать и сопровождать.
С декораторами код основной функции становится компактным, поскольку весь код, предназначенный для расширения ее возможностей, пишется за ее пределами. За счет декораторов можно добавлять в код новые возможности, не усложняя его.
Сейчас предложение к стандарту о декораторах классов находится на 2-м этапе рассмотрения, и к этому предложению еще может добавиться множество полезных дополнений.
Совет. Делитесь компонентами многоразового использования для разных проектов на платформе Bit(Github). Это простой способ документировать и систематизировать независимые компоненты из любых проектов и делиться ими.
Платформа открывает широкие возможности повторного использования кода, совместной работы над независимыми компонентами и разработки масштабируемых приложений.
Bitподдерживает Node, TypeScript, React, Vue, Angular и другие фреймворки JS.
Примеры React-компонентов многоразового использования на Bit.devДекораторы функций
Что такое декораторы функций?
Декораторы функций это такие же функции. Они принимают функцию в качестве аргумента и возвращают другую функцию, которая расширяет поведение функции-аргумента. Новая функция не изменяет функцию-аргумент, но использует ее в своем теле. Как я уже говорил, это во многом напоминает функции высшего порядка.
Как работают декораторы функций?
Рассмотрим пример.
Проверка аргументов обычная практика в программировании. В таких языках, как Java, если функция ожидает два аргумента, а получает три, генерируется исключение. Но в JavaScript ошибки не будет, поскольку лишние параметры попросту игнорируются. Такое поведение функций иногда раздражает, но может быть и полезным.
Для того чтобы убедиться в допустимости аргументов, нужно проверить их на входе. Это простая операция, в которой проверяется, что у каждого параметра надлежащий тип данных, а их количество не превышает ожидаемого функцией.
Однако повторение одной и той же операции для нескольких функций может привести к повторению кода, поэтому для проверки аргументов лучше написать декоратор, который затем можно будет многократно использовать с любыми функциями.
//decorator functionconst 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 functionlet multiply = function(a,b){return a*b;}//decorated multiply function that only accepts the required number of params and only integersmultiply = allArgsValid(multiply);multiply(6, 8);//48multiply(6, 8, 7);//Error: Only submit required number of paramsmultiply(3, null);//TypeError: Argument cannot be a non-integermultiply('',4);//TypeError: Argument cannot be a non-integer
В этом примере мы используем функцию-декоратор
allArgsValid
, которая принимает в качестве аргумента
функцию. Декоратор возвращает другую функцию, которая обертывает
функцию-аргумент. При этом функция-аргумент вызывается только в том
случае, когда передаваемые в нее аргументы являются целыми числами.
Иначе генерируется ошибка. Декоратор также проверяет количество
передаваемых параметров: оно должно строго совпадать с количеством,
которое ожидает функция.
Затем мы объявляем переменную multiply
и в качестве
значения присваиваем ей функцию, которая перемножает два числа. Мы
передаем эту функцию умножения в функцию-декоратор
allArgsValid
, которая, как мы уже знаем, возвращает
другую функцию. Возвращаемая функция снова присваивается переменной
multiply
. Таким образом, разработанный функционал
можно будет без труда использовать повторно.
//ordinary add functionlet add = function(a,b){return a+b;}//decorated add function that only accepts the required number of params and only integersadd = allArgsValid(add);add(6, 8);//14add(3, null);//TypeError: Argument cannot be a non-integeradd('',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); };}@logclass Book { constructor(name, ISBN) { this.name = name; this.ISBN = ISBN; } getBook() { return `[${this.name}][${this.ISBN}]`; }}let obj = new Book("HP", "1245-533552");//Constructor Calledconsole.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 tableconsole.log(obj.getBook());//[HP][1245-533552]
Декораторы свойств класса
В их синтаксисе, как и в синтаксисе декораторов классов,
используется префикс @
. В декораторы свойств класса
можно передавать параметры точно так же, как в другие декораторы,
которые мы рассмотрели на примерах.
Декораторы методов класса
Аргументы, которые передаются в декоратор метода класса, будут отличаться от аргументов декоратора класса. Декоратор метода класса получает не один, а три параметра:
-
target
объект, в котором содержатся конструктор и методы, объявленные внутри класса; -
name
имя метода, для которого вызывается декоратор; -
descriptor
объект дескриптора, соответствующий методу, для которого вызывается декоратор. Одескрипторах свойств можно подробнее почитать здесь.
Большинство манипуляций будет выполняться с аргументом descriptor. При использовании с методом класса объект дескриптора имеет 4 атрибута:
-
configurable
логическое значение, которое определяет, можно ли изменять свойства дескриптора; -
enumerable
логическое значение, которое определяет, будет ли свойство видимым при перечислении свойств объекта; -
value
значение свойства. В нашем случае это функция; -
writable
логическое значение, которое определяет, возможна ли перезапись свойства.
Рассмотрим пример с классом Book
.
//readonly decorator functionfunction 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 decoratorclass 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".