Учитывая введение стандарта ES2015+, и то, что транспиляция в наше время — обычное дело, многие программисты сталкиваются с новыми возможностями JavaScript в реальном коде и в учебных материалах. Одна из таких возможностей — декораторы. Во всех этих новшествах немудрено и запутаться, поэтому сегодня поговорим о том, что такое декораторы, и о том, как их использовать для того, чтобы сделать код чище и понятнее.
Декораторы обрели популярность благодаря их применению в Angular 2+. В Angular этот функционал реализуется средствами TypeScript. Сейчас предложение по введению декораторов в JavaScript находится в состоянии Stage 2 Draft. Это означает, что работа над ними, в основном, завершена, но они всё ещё могут подвергаться изменениям. Декораторы должны стать частью следующего обновления языка.
В простейшем виде декоратор — это способ оборачивания одного фрагмента кода в другой. Буквально — «декорирование» фрагмента кода.
Об этой концепции вы, возможно, слышали ранее, как о «Функциональной композиции» или о «Функциях высшего порядка».
Подобное вполне реализуемо стандартными средствами JavaScript. Выглядит это как вызов некоей функции, которая оборачивает другую:
В этом примере показано, как создаётся новая функция, которая назначается константе
Декораторы в JavaScript используют специальный синтаксис, они имеют префикс в виде символа
К одному фрагменту кода можно применять столько декораторов, сколько нужно, они будут задействованы в порядке их следования в коде.
Например:
Здесь показано объявление класса и применение трёх декораторов. Два из них относятся к самому классу, и один — к свойству класса. Вот каковы роли этих декораторов:
Сегодня использование декораторов требует применение транспилятора, так как их пока не поддерживают ни браузеры, ни Node.js.
Если вы используете Babel, для работы с декораторами можно обратиться к плагину transform-decorators-legacy.
Обратите внимание на то, что в названии этого плагина используется слово «legacy», которое можно трактовать как указание на некую устаревшую технологию. Дело тут в том, что здесь поддерживается то, как декораторы обрабатывает Babel 5. Этот подход может отличаться от той формы, которая, в итоге, будет стандартизирована.
Функциональная композиция в JavaScript без особых проблем реализуется и стандартными средствами. Однако, тот же подход либо очень сложно, либо невозможно применить к другим программным конструкциям, например — к классам и их свойствам. Предложенные нововведения позволяют использовать декораторы и с классами, и с их свойствами. Вероятно, в будущих версиях JavaScript можно ожидать дальнейшего развития декораторов.
Использование декораторов, помимо прочих возможностей, означает более чёткий синтаксис, это ведёт к более ясному выражению намерений программиста, использующего эту технику.
Сейчас поддерживаемые типы декораторов — это декораторы классов и членов классов — свойств, методов, геттеров и сеттеров. На самом деле, декораторы — это всего лишь функции, которые вызываются с некоторыми сведениями о декорируемых элементах и возвращают другие функции.
Декораторы выполняются один раз при запуске программы, декорируемый код заменяется возвращаемым значением.
Декораторы членов класса применяются к свойствам, методам, геттерам и сеттерам.
Эти функции-декораторы вызываются с тремя параметрами:
Вот классический пример, который демонстрирует использование декоратора
Декоратор устанавливает флаг
Затем этот декоратор используется с членами класса следующим образом:
Как видно по сообщению об ошибке, декоратор сработал так, как ожидается. Мы можем пойти и дальше, например, изменив поведение декорируемой функции, фактически, заменив её новой функцией. Давайте, с помощью декоратора, выведем в лог аргументы функции, и то, что она возвращает:
Эта конструкция заменяет метод новым, который логирует аргументы, вызывает исходный метод, а затем логирует то, что он возвращает.
Обратите внимание на то, что здесь мы использовали оператор расширения для того, чтобы автоматически создать массив со всеми переданными методу аргументами. Это — современная альтернатива свойству функции
Посмотрим на всё это в действии:
Можно заметить, что в тексте функции-декоратора приходится использовать необычный синтаксис для исполнения декорируемого метода. Об этом, на самом деле, можно написать целую статью, но, если кратко, то метод
Пойдя ещё дальше, можно устроить всё так, чтобы декоратор принимал аргументы для собственных целей. Например, перепишем декоратор
Код усложнился, но если с ним разобраться, окажется, что происходит тут следующее:
Возвращённая функция идентична декоратору
Пользоваться всем этим можно так:
Сразу видно, что такой подход позволяет различать строки логов благодаря назначенному тегу.
Здесь выполняется вызов функции вида
Декораторы классов применяются ко всему определению класса. Функция-декоратор вызывается с единственным параметром, которым является декорируемая функция-конструктор класса.
Обратите внимание на то, что декоратор применяется к функции-конструктору, а не к каждому экземпляру класса в момент его создания. Поэтому для того, чтобы по-разному декорировать разные экземпляры одного и того же класса, понадобится, перед их созданием, самостоятельно заботиться о декорировании конструктора.
В целом, декораторы классов менее полезны, чем декораторы членов классов, так как всё, что тут можно сделать, сводится к замене конструктора класса.
Возвращаясь к примеру с логированием, напишем декоратор, который будет выводить параметры конструктора:
Тут, в качестве аргумента, принимается класс и возвращается новая функция, которая будет действовать как конструктор. В нашем случае она просто логирует аргументы и возвращает новый экземпляр класса, созданного с этими аргументами.
Например:
Как видно, при выполнении конструктора класса
Для передачи параметров декораторам классов можно воспользоваться уже описанным подходом:
Вот несколько примеров использования декораторов в популярных библиотеках.
Есть отличная библиотека Core Decorators, которая предоставляет готовые к использованию декораторы общего назначения. Среди поддерживаемой ими функциональности — тайминг вызовов методов, уведомления об устаревших конструкциях, проверка того, является ли некий объект неизменяемым.
В React найдено хорошее применение концепции компонентов высшего порядка. Это — компоненты React, написанные как функции и служащие обёртками для других компонентов.
Они — идеальные кандидаты на использование в качестве декораторов, так как для того, чтобы использовать их в таком качестве, потребуются минимальные усилия. Например, в библиотеке Redux есть функция
Если же переписать это с использованием декораторов, получится следующее:
Функционал получился тот же, но выглядит всё это гораздо симпатичнее.
Декораторы широко используются в библиотеке MobX. Например, для обеспечения нужного поведения системы надо просто добавлять к полям декораторы
Мы поговорили о том, как создавать и использовать декораторы в JavaScript, в частности — рассмотрели особенности работы с декораторами членов класса. Такой подход позволяет писать вспомогательный код, представленный функциями-декораторами, который можно применять для изменения поведения методов различных классов. Синтаксис декораторов позволяет упростить тексты программ, сделать их чище и понятнее. С декораторами в JavaScript можно работать уже сегодня, они нашли применение в популярных библиотеках. Однако, полагаем, после того, как их напрямую будут поддерживать браузеры и Node.js, у них найдётся множество новых поклонников.
Уважаемые читатели! А вы уже пользуетесь декораторами в своих JS-проектах?
Декораторы обрели популярность благодаря их применению в Angular 2+. В Angular этот функционал реализуется средствами TypeScript. Сейчас предложение по введению декораторов в JavaScript находится в состоянии Stage 2 Draft. Это означает, что работа над ними, в основном, завершена, но они всё ещё могут подвергаться изменениям. Декораторы должны стать частью следующего обновления языка.
Что такое декоратор?
В простейшем виде декоратор — это способ оборачивания одного фрагмента кода в другой. Буквально — «декорирование» фрагмента кода.
Об этой концепции вы, возможно, слышали ранее, как о «Функциональной композиции» или о «Функциях высшего порядка».
Подобное вполне реализуемо стандартными средствами JavaScript. Выглядит это как вызов некоей функции, которая оборачивает другую:
function doSomething(name) {
console.log('Hello, ' + name);
}
function loggingDecorator(wrapped) {
return function() {
console.log('Starting');
const result = wrapped.apply(this, arguments);
console.log('Finished');
return result;
}
}
const wrapped = loggingDecorator(doSomething);
В этом примере показано, как создаётся новая функция, которая назначается константе
wrapped
. Эта функция может быть вызвана точно так же, как и функция doSomething
, и делать она будет то же самое. Разница заключается в том, что до и после вызова оборачиваемой функции будет выполнено логирование. Вот что произойдёт, если поэкспериментировать с функциями doSomething
и wrapped
.doSomething('Graham');
// Hello, Graham
wrapped('Graham');
// Starting
// Hello, Graham
// Finished
Как применять декораторы в JavaScript?
Декораторы в JavaScript используют специальный синтаксис, они имеют префикс в виде символа
@
, их размещают непосредственно перед кодом, который хотят декорировать.К одному фрагменту кода можно применять столько декораторов, сколько нужно, они будут задействованы в порядке их следования в коде.
Например:
@log()
@immutable()
class Example {
@time('demo')
doSomething() {
}
}
Здесь показано объявление класса и применение трёх декораторов. Два из них относятся к самому классу, и один — к свойству класса. Вот каковы роли этих декораторов:
@log
может логировать все обращения к классу.
@immutable
способен сделать класс иммутабельным — возможно, он вызоветObject.freeze
для новых экземпляров класса.
@time
записывает сведения о длительности исполнения методов и выводит эти сведения в лог с уникальным тегом.
Сегодня использование декораторов требует применение транспилятора, так как их пока не поддерживают ни браузеры, ни Node.js.
Если вы используете Babel, для работы с декораторами можно обратиться к плагину transform-decorators-legacy.
Обратите внимание на то, что в названии этого плагина используется слово «legacy», которое можно трактовать как указание на некую устаревшую технологию. Дело тут в том, что здесь поддерживается то, как декораторы обрабатывает Babel 5. Этот подход может отличаться от той формы, которая, в итоге, будет стандартизирована.
Зачем нужны декораторы?
Функциональная композиция в JavaScript без особых проблем реализуется и стандартными средствами. Однако, тот же подход либо очень сложно, либо невозможно применить к другим программным конструкциям, например — к классам и их свойствам. Предложенные нововведения позволяют использовать декораторы и с классами, и с их свойствами. Вероятно, в будущих версиях JavaScript можно ожидать дальнейшего развития декораторов.
Использование декораторов, помимо прочих возможностей, означает более чёткий синтаксис, это ведёт к более ясному выражению намерений программиста, использующего эту технику.
Разные типы декораторов
Сейчас поддерживаемые типы декораторов — это декораторы классов и членов классов — свойств, методов, геттеров и сеттеров. На самом деле, декораторы — это всего лишь функции, которые вызываются с некоторыми сведениями о декорируемых элементах и возвращают другие функции.
Декораторы выполняются один раз при запуске программы, декорируемый код заменяется возвращаемым значением.
▍Декораторы членов класса
Декораторы членов класса применяются к свойствам, методам, геттерам и сеттерам.
Эти функции-декораторы вызываются с тремя параметрами:
target —
класс, в котором находится декорируемый член класса.name —
имя члена класса.
descriptor —
дескриптор члена класса. Это, по существу, объект, который был бы передан методу Object.defineProperty.
Вот классический пример, который демонстрирует использование декоратора
@readonly
. Этот декоратор реализован так:function readonly(target, name, descriptor) {
descriptor.writable = false;
return descriptor;
}
Декоратор устанавливает флаг
writable
дескриптора свойства в значение false
.Затем этот декоратор используется с членами класса следующим образом:
class Example {
a() {}
@readonly
b() {}
}
const e = new Example();
e.a = 1;
e.b = 2;
// TypeError: Cannot assign to read only property 'b' of object '#<Example>'
Как видно по сообщению об ошибке, декоратор сработал так, как ожидается. Мы можем пойти и дальше, например, изменив поведение декорируемой функции, фактически, заменив её новой функцией. Давайте, с помощью декоратора, выведем в лог аргументы функции, и то, что она возвращает:
function log(target, name, descriptor) {
const original = descriptor.value;
if (typeof original === 'function') {
descriptor.value = function(...args) {
console.log(`Arguments: ${args}`);
try {
const result = original.apply(this, args);
console.log(`Result: ${result}`);
return result;
} catch (e) {
console.log(`Error: ${e}`);
throw e;
}
}
}
return descriptor;
}
Эта конструкция заменяет метод новым, который логирует аргументы, вызывает исходный метод, а затем логирует то, что он возвращает.
Обратите внимание на то, что здесь мы использовали оператор расширения для того, чтобы автоматически создать массив со всеми переданными методу аргументами. Это — современная альтернатива свойству функции
arguments
.Посмотрим на всё это в действии:
class Example {
@log
sum(a, b) {
return a + b;
}
}
const e = new Example();
e.sum(1, 2);
// Arguments: 1,2
// Result: 3
Можно заметить, что в тексте функции-декоратора приходится использовать необычный синтаксис для исполнения декорируемого метода. Об этом, на самом деле, можно написать целую статью, но, если кратко, то метод
apply
позволяет вызывать функцию, задавая значение this
и аргументы, которые будут ей переданы.Пойдя ещё дальше, можно устроить всё так, чтобы декоратор принимал аргументы для собственных целей. Например, перепишем декоратор
log
следующим образом:function log(name) {
return function decorator(t, n, descriptor) {
const original = descriptor.value;
if (typeof original === 'function') {
descriptor.value = function(...args) {
console.log(`Arguments for ${name}: ${args}`);
try {
const result = original.apply(this, args);
console.log(`Result from ${name}: ${result}`);
return result;
} catch (e) {
console.log(`Error from ${name}: ${e}`);
throw e;
}
}
}
return descriptor;
};
}
Код усложнился, но если с ним разобраться, окажется, что происходит тут следующее:
- Имеется функция
log
, которая принимает единственный аргумент —name
. - Эта функция возвращает ещё одну функцию, которая и является декоратором.
Возвращённая функция идентична декоратору
log
, который мы описывали выше, за исключением того, что она использует параметр name
из внешней функции.Пользоваться всем этим можно так:
class Example {
@log('some tag')
sum(a, b) {
return a + b;
}
}
const e = new Example();
e.sum(1, 2);
// Arguments for some tag: 1,2
// Result from some tag: 3
Сразу видно, что такой подход позволяет различать строки логов благодаря назначенному тегу.
Здесь выполняется вызов функции вида
log('some tag')
, а затем то, что было возвращено из этого вызова, используется как декоратор для метода sum
.▍Декораторы классов
Декораторы классов применяются ко всему определению класса. Функция-декоратор вызывается с единственным параметром, которым является декорируемая функция-конструктор класса.
Обратите внимание на то, что декоратор применяется к функции-конструктору, а не к каждому экземпляру класса в момент его создания. Поэтому для того, чтобы по-разному декорировать разные экземпляры одного и того же класса, понадобится, перед их созданием, самостоятельно заботиться о декорировании конструктора.
В целом, декораторы классов менее полезны, чем декораторы членов классов, так как всё, что тут можно сделать, сводится к замене конструктора класса.
Возвращаясь к примеру с логированием, напишем декоратор, который будет выводить параметры конструктора:
function log(Class) {
return (...args) => {
console.log(args);
return new Class(...args);
};
}
Тут, в качестве аргумента, принимается класс и возвращается новая функция, которая будет действовать как конструктор. В нашем случае она просто логирует аргументы и возвращает новый экземпляр класса, созданного с этими аргументами.
Например:
@log
class Example {
constructor(name, age) {
}
}
const e = new Example('Graham', 34);
// [ 'Graham', 34 ]
console.log(e);
// Example {}
Как видно, при выполнении конструктора класса
Example
будет выполнено логирование аргументов конструктора, которые используются при создании экземпляра этого класса. Это — именно то, чего мы добивались.Для передачи параметров декораторам классов можно воспользоваться уже описанным подходом:
function log(name) {
return function decorator(Class) {
return (...args) => {
console.log(`Arguments for ${name}: args`);
return new Class(...args);
};
}
}
@log('Demo')
class Example {
constructor(name, age) {}
}
const e = new Example('Graham', 34);
// Arguments for Demo: args
console.log(e);
// Example {}
Декораторы в реальных проектах
Вот несколько примеров использования декораторов в популярных библиотеках.
▍Библиотека Core Decorators
Есть отличная библиотека Core Decorators, которая предоставляет готовые к использованию декораторы общего назначения. Среди поддерживаемой ими функциональности — тайминг вызовов методов, уведомления об устаревших конструкциях, проверка того, является ли некий объект неизменяемым.
▍Библиотека React
В React найдено хорошее применение концепции компонентов высшего порядка. Это — компоненты React, написанные как функции и служащие обёртками для других компонентов.
Они — идеальные кандидаты на использование в качестве декораторов, так как для того, чтобы использовать их в таком качестве, потребуются минимальные усилия. Например, в библиотеке Redux есть функция
connect
, которая используется для подключения компонентов React. Без декораторов работа с этой функцией может выглядеть так:class MyReactComponent extends React.Component {}
export default connect(mapStateToProps, mapDispatchToProps)(MyReactComponent);
Если же переписать это с использованием декораторов, получится следующее:
@connect(mapStateToProps, mapDispatchToProps)
export default class MyReactComponent extends React.Component {}
Функционал получился тот же, но выглядит всё это гораздо симпатичнее.
▍Библиотека MobX
Декораторы широко используются в библиотеке MobX. Например, для обеспечения нужного поведения системы надо просто добавлять к полям декораторы
Observable
или Computed
, а с классами использовать декоратор Observers
.Итоги
Мы поговорили о том, как создавать и использовать декораторы в JavaScript, в частности — рассмотрели особенности работы с декораторами членов класса. Такой подход позволяет писать вспомогательный код, представленный функциями-декораторами, который можно применять для изменения поведения методов различных классов. Синтаксис декораторов позволяет упростить тексты программ, сделать их чище и понятнее. С декораторами в JavaScript можно работать уже сегодня, они нашли применение в популярных библиотеках. Однако, полагаем, после того, как их напрямую будут поддерживать браузеры и Node.js, у них найдётся множество новых поклонников.
Уважаемые читатели! А вы уже пользуетесь декораторами в своих JS-проектах?