JavaScript предоставляет удивительно гибкие возможности по работе с функциями: их можно передавать, в них можно записывать данные как в объекты, у них есть свои встроенные методы…
Конечно, этим нужно уметь пользоваться. В этой главе, чтобы более глубоко понимать работу с функциями, мы рассмотрим создание функций-обёрток или, иначе говоря, «декораторов».
Декоратор – приём программирования, который позволяет взять существующую функцию и изменить/расширить её поведение.
Декоратор получает функцию и возвращает обёртку, которая делает что-то своё «вокруг» вызова основной функции.
bind – привязка контекста
Один простой декоратор вы уже видели ранее – это функция bind:
function
bind
(
func,
context
)
{
return
function
(
)
{
return
func
.
apply
(
context,
arguments)
;
}
;
}
Вызов bind(func, context)
возвращает обёртку, которая ставит this
и передаёт основную работу функции func
.
Декоратор-таймер
Создадим более сложный декоратор, замеряющий время выполнения функции.
Он будет называться timingDecorator
и получать функцию вместе с «названием таймера», а возвращать – функцию-обёртку, которая измеряет время и прибавляет его в специальный объект timer
по свойству-названию.
Использование:
function
f
(
x
)
{
}
// любая функция
var
timers =
{
}
;
// объект для таймеров
// отдекорировали
f =
timingDecorator
(
f,
"myFunc"
)
;
// запускаем
f
(
1
)
;
f
(
2
)
;
f
(
3
)
;
// функция работает как раньше, но время подсчитывается
alert
(
timers.
myFunc )
;
// общее время выполнения всех вызовов f
При помощи декоратора timingDecorator
мы сможем взять произвольную функцию и одним движением руки прикрутить к ней измеритель времени.
Его реализация:
var
timers =
{
}
;
// прибавит время выполнения f к таймеру timers[timer]
function
timingDecorator
(
f,
timer
)
{
return
function
(
)
{
var
start =
performance.
now
(
)
;
var
result =
f
.
apply
(
this
,
arguments)
;
// (*)
if
(
!
timers[
timer]
)
timers[
timer]
=
0
;
timers[
timer]
+=
performance.
now
(
)
-
start;
return
result;
}
}
// функция может быть произвольной, например такой:
var
fibonacci
=
function
f
(
n
)
{
return
(
n >
2
)
?
f
(
n -
1
)
+
f
(
n -
2
)
:
1
;
}
// использование: завернём fibonacci в декоратор
fibonacci =
timingDecorator
(
fibonacci,
"fibo"
)
;
// неоднократные вызовы...
alert
(
fibonacci
(
10
)
)
;
// 55
alert
(
fibonacci
(
20
)
)
;
// 6765
// ...
// в любой момент можно получить общее количество времени на вызовы
alert
(
timers.
fibo +
'мс'
)
;
Обратим внимание на строку (*)
внутри декоратора, которая и осуществляет передачу вызова:
var
result =
f
.
apply
(
this
,
arguments)
;
// (*)
Этот приём называется «форвардинг вызова» (от англ. forwarding): текущий контекст и аргументы через apply
передаются в функцию f
, так что изнутри f
всё выглядит так, как была вызвана она напрямую, а не декоратор.
Декоратор для проверки типа
В JavaScript, как правило, пренебрегают проверками типа. В функцию, которая должна получать число, может быть передана строка, булево значение или даже объект.
Например:
function
sum
(
a,
b
)
{
return
a +
b;
}
// передадим в функцию для сложения чисел нечисловые значения
alert
(
sum
(
true
,
{
name
:
"Вася"
,
age
:
35
}
)
)
;
// true[Object object]
Функция «как-то» отработала, но в реальной жизни передача в sum
подобных значений, скорее всего, будет следствием программной ошибки. Всё-таки sum
предназначена для суммирования чисел, а не объектов.
Многие языки программирования позволяют прямо в объявлении функции указать, какие типы данных имеют параметры. И это удобно, поскольку повышает надёжность кода.
В JavaScript же проверку типов приходится делать дополнительным кодом в начале функции, который во-первых обычно лень писать, а во-вторых он увеличивает общий объем текста, тем самым ухудшая читаемость.
Декораторы способны упростить рутинные, повторяющиеся задачи, вынести их из кода функции.
Например, создадим декоратор, который принимает функцию и массив, который описывает для какого аргумента какую проверку типа применять:
// вспомогательная функция для проверки на число
function
checkNumber
(
value
)
{
return
typeof
value ==
'number'
;
}
// декоратор, проверяющий типы для f
// второй аргумент checks - массив с функциями для проверки
function
typeCheck
(
f,
checks
)
{
return
function
(
)
{
for
(
var
i =
0
;
i <
arguments.
length;
i++
)
{
if
(
!
checks[
i]
(
arguments[
i]
)
)
{
alert
(
"Некорректный тип аргумента номер "
+
i )
;
return
;
}
}
return
f
.
apply
(
this
,
arguments)
;
}
}
function
sum
(
a,
b
)
{
return
a +
b;
}
// обернём декоратор для проверки
sum =
typeCheck
(
sum,
[
checkNumber,
checkNumber]
)
;
// оба аргумента - числа
// пользуемся функцией как обычно
alert
(
sum
(
1
,
2
)
)
;
// 3, все хорошо
// а вот так - будет ошибка
sum
(
true
,
null
)
;
// некорректный аргумент номер 0
sum
(
1
,
[
"array"
,
"in"
,
"sum?!?"
]
)
;
// некорректный аргумент номер 1
Конечно, этот декоратор можно ещё расширять, улучшать, дописывать проверки, но… Вы уже поняли принцип, не правда ли?
Один раз пишем декоратор и дальше просто применяем эту функциональность везде, где нужно.
Декоратор проверки доступа
И наконец посмотрим ещё один, последний пример.
Предположим, у нас есть функция isAdmin()
, которая возвращает true
, если у посетителя есть права администратора.
Можно создать декоратор checkPermissionDecorator
, который добавляет в любую функцию проверку прав:
Например, создадим декоратор checkPermissionDecorator(f)
. Он будет возвращать обёртку, которая передаёт вызов f
в том случае, если у посетителя достаточно прав:
function
checkPermissionDecorator
(
f
)
{
return
function
(
)
{
if
(
isAdmin
(
)
)
{
return
f
.
apply
(
this
,
arguments)
;
}
alert
(
'Недостаточно прав'
)
;
}
}
Использование декоратора:
function
save
(
)
{
...
}
save =
checkPermissionDecorator
(
save)
;
// Теперь вызов функции save() проверяет права
Итого
Декоратор – это обёртка над функцией, которая модифицирует её поведение. При этом основную работу по-прежнему выполняет функция.
Декораторы можно не только повторно использовать, но и комбинировать!
Это кардинально повышает их выразительную силу. Декораторы можно рассматривать как своего рода «фичи» или возможности, которые можно «нацепить» на любую функцию. Можно один, а можно несколько.
Скажем, используя декораторы, описанные выше, можно добавить к функции возможности по проверке типов данных, замеру времени и проверке доступа буквально одной строкой, не залезая при этом в её код, то есть (!) не увеличивая его сложность.
Предлагаю вашему вниманию задачи, которые помогут выяснить, насколько вы разобрались в декораторах. Далее в учебнике мы ещё встретимся с ними.
Комментарии
<code>
, для нескольких строк кода — тег<pre>
, если больше 10 строк — ссылку на песочницу (plnkr, JSBin, codepen…)