Декораторы — это объекты, которые используются для динамического добавления дополнительной функциональности к другому объекту без изменения реализации этого объекта.
Пример использования:
@filterMales // Это декоратор
class MyClass {
constructor(children) {
this.children = children
}
}
Декораторы — это синтаксический сахар. Если вы работали с JavaScript, то, вероятно, уже сталкивались с их реализацией. Они просты в написании, но эффективны в использовании.
Рассмотрим несколько примеров декораторов в JavaScript и узнаем, как можно применять их в коде.
Существует несколько случаев применения:
Как было сказано ранее, один из сценариев использования декораторов — динамическое добавление дополнительной логики к объектам без необходимости использовать такие альтернативы, как наследование.
Примечание: декораторы могут вводить функции в объекты не заметно для остальных частей кода.
Допустим, у нас есть класс Frog
, который будет реализовывать метод lick
. У лягушек есть зубы, поэтому мы также реализуем метод getTeeths
, чтобы вернуть количество зубов, которое у них есть.
Реализация может выглядеть следующим образом:
function Frog(name) {
this.name = name
}
Frog.prototype.getTeeths = function() {
return 2
}
Frog.prototype.lick = function(target) {
console.log(`I'm going lick you, ${target.name}. You better taste delicious`)
}
// Или с классами
class Frog {
constructor(name) {
this.name = name
}
getTeeths() {
return 2
}
lick(target) {
console.log(
`I'm going lick you, ${target.name}. You better taste delicious`,
)
}
}
В реальности существуют и другие разновидности лягушек, например жаба. Жаба относится к лягушкам, но лягушка не является жабой. Это означает, что между ними должны быть отличительные особенности, которые нельзя смешивать.
Поскольку жаба — это лягушка, то можно создать декоратор withToad
, который при необходимости будет представлять экземпляр frog для обозначения жаб.
Помните, что декоратор должен лишь расширять или добавлять дополнительное поведение к элементу, но не изменять его реализацию.
Реализация декоратора withToad выглядит довольно просто:
function withToad(frog) {
frog.getTeeths = function() {
return 0
}
}
const mikeTheFrog = new Frog('mike')
withToad(mikeTheFrog)
console.log(mikeTheFrog.getTeeths())
Декоратор withToad
повторно реализует getTeeths
, возвращающий 0
, поскольку у жаб нет зубов. При использовании этого декоратора лягушка «преобразуется» в жабу.
Достичь той же цели можно с помощью метода subclass с наследованием, как показано ниже:
function Toad(name) {
Frog.call(this, name)
this.getTeeths = function() {
return 0
}
}
const kellyTheToad = new Toad('kelly')
// или с помощью классов
class Toad extends Frog {
getTeeths() {
return 0
}
}
const kellyTheToad = new Toad('kelly')
Разница между этими двумя подходами заключается в том, что при использовании декораторов не нужно создавать классы для жаб.
Теперь переходим к более интересным примерам.
Допустим, мы создаем приложение, которое поддерживает различные предварительно определенные пользовательские темы, с помощью которых можно изменять стиль панели управления.
Мы реализуем Theme
с помощью метода createStylesheet
для создания совместимой таблицы стилей, а также applyStyles
для анализа и применения этой таблицы к DOM:
function Theme() {}
Theme.prototype.createStylesheet = function() {
return {
header: {
color: '#333',
fontStyle: 'italic',
fontFamily: 'Roboto, sans-serif',
},
background: {
backgroundColor: '#fff',
},
button: {
backgroundColor: '#fff',
color: '#333',
},
color: '#fff',
}
}
Theme.prototype.applyStylesheet = function(stylesheet) {
const bodyElem = document.querySelector('body')
const headerElem = document.getElementById('header')
const buttonElems = document.querySelectorAll('button')
this.applyStyles(bodyElem, stylesheet.background)
this.applyStyles(headerElem, stylesheet.header)
buttonElems.forEach((buttonElem) => {
this.applyStyles(buttonElem, stylesheet.button)
})
}
Theme.prototype.applyStyles = function(elem, styles) {
for (let key in styles) {
if (styles.hasOwnProperty(key)) {
elem.style[key] = styles[key]
}
}
}
Мы определили API Theme
и теперь можем создать таблицу стилей следующим образом:
На данный момент stylesheet
выглядит так:
{
"header": {
"color": "#333",
"fontStyle": "italic",
"fontFamily": "Roboto, sans-serif"
},
"background": { "backgroundColor": "#fff" },
"button": { "backgroundColor": "#fff", "color": "#333" },
"color": "#fff"
}
Теперь ее можно использовать для преображения веб-страницы:
theme.applyStylesheet(stylesheet)Как сделать так, чтобы theme
возвращал пользовательскую тему при вызове createStylesheet
, которую можно использовать для расширения вместо темы по умолчанию?
В таком случае могут пригодиться декораторы, так как с их помощью можно вернуть другую предварительно определенную тему по умолчанию.
Мы создадим декоратор, чтобы применить тему blood
, преобразующую Theme
так, чтобы он генерировал таблицу стилей по умолчанию, которая будет представлять тему blood
вместо оригинала.
Назовем этот декоратор bloodTheme
:
function bloodTheme(originalTheme) {
const originalStylesheet = originalTheme.createStylesheet()
originalTheme.createStylesheet = function() {
return {
name: 'blood',
...originalStylesheet,
header: {
...originalStylesheet.header,
color: '#fff',
fontStyle: 'italic',
},
background: {
...originalStylesheet.background,
color: '#fff',
backgroundColor: '#C53719',
},
button: {
...originalStylesheet.button,
backgroundColor: 'maroon',
color: '#fff',
},
primary: '#C53719',
secondary: 'maroon',
textColor: '#fff',
}
}
}
Теперь нужно добавить в theme
только одну строку:
const theme = new Theme()
bloodTheme(theme) // Applying the decorator
const stylesheet = theme.createStylesheet()
console.log(stylesheet)
Теперь тема предоставляет таблицу стилей blood
по умолчанию:
{
"name": "blood",
"header": {
"color": "#fff",
"fontStyle": "italic",
"fontFamily": "Roboto, sans-serif"
},
"background": { "backgroundColor": "#C53719", "color": "#fff" },
"button": { "backgroundColor": "maroon", "color": "#fff" },
"color": "#fff",
"primary": "#C53719",
"secondary": "maroon",
"textColor": "#fff"
}
Как видите, код/реализация theme
не изменились. Применение пользовательской таблицы стилей также не изменилось:
Теперь веб-страница раскрашена красным цветом:
Мы можем создать любое количество тем и применять их в любое удобное время. Это означает, что код остается открытым для таких плагинов, как пользовательские темы.
Декораторы также стоит использовать при необходимости временно применить поведение к объектам, которые будут удалены в дальнейшем.
Например, в рождественский сезон можно с легкостью создать рождественскую таблицу стилей и применить ее в качестве декоратора, а затем удалить из кода по окончанию сезона.
Чтобы вернуться к исходной таблице стилей нужно просто удалить строку bloodTheme(theme)
.
Еще один пример использования декораторов — при создании подклассов в большом коде.
Однако этот случай не является большой проблемой в JavaScript (если только вы не используете реализации наследования классов), в отличие от статических языков, таких как Java.
Еще один полезный вариант использования — создание декоратора режима отладки, который будет регистрировать все события, происходящие в консоли. Например, декоратор debugTheme
, который пригодится в режиме разработки:
function debugTheme(originalTheme) {
const stylesheet = originalTheme.createStylesheet()
console.log(
'%cStylesheet created:',
'color:green;font-weight:bold;',
stylesheet,
)
if (!stylesheet.primary) {
console.warn(
'A stylesheet was created without a primary theme color. There may be layout glitches.',
)
}
}
const theme = new Theme()
bloodTheme(theme)
if (process.env.NODE_ENV === 'development') debugTheme(theme)
Теперь консоль предоставляет полезную информацию при запуске приложения в режиме development
:
Перевод статьи jsmanifest: Learn About Decorators in JavaScript
Комментарии