Декораторы в JavaScript
С давних времён использую декораторы в JavaScript. Недавно увидел хабротопик про примеси, который натолкнул меня на мысль поделиться собственным опытом, ибо технологии немного похожие.
Что меня не устраивает в известных реализациях?
Реализации, предлагаемые по первым ссылкам в Google, работают не тем образом, как это работает в Python. Во многих статьях предлагается создать объект, заполнить его поля функциями и осуществлять вызовы отдекорированных через эти поля.
На первой странице есть несколько ссылок, где используются методы, сходные с моими,
Есть ещё много реализаций, но они мне неинтересны.
Расскажу наиболее правильную с моей точки зрения.
Нормальный декоратор
Итак, что такое декоратор?
Декоратор, это функция, которая добавляет функции-аргументу функционала.
Пример из Python:
def superfunc(n=2): print("Megclosure created") def clos(func): print("Megclosure used") def clos1(*args, **kwargs): print("Megclosured function",func.__name__) res = func(*args, **kwargs) return (res**n) return clos1 return clos def a(par): return par+9; b = (superfunc())(a) @superfunc(3) def c(txt): return len(txt)+1 print ( b(1),c("abc"*3))
Что мы тут делаем?
Мы тут создаём функцию, которая создаёт другую функцию, которая выполняет то, что нам нужно, и вызывает декорируемую функцию.
Как это будет на JavaScript
Итак, мы хотим вызвать функцию, передав ей функцию и, возможно, дополнительные параметры, и получить отдекорированную функцию b, т.е.
function decorator() < . >. function a() < . >. var b=decorator(a, arg1, arg2. argN);
Приведу участок кода из одной своей библиотеки.
function withVars(f, variables) < var args = Array.prototype.slice.call(arguments, 1);// получаем массив аргументов функции, за исключением самой функции, причём именно МАССИВ return function ()/ замыкаем f и args, создаём отдекорированную функцию var p = arguments;// мы к arguments ничего добавить не сможем . Array.prototype.push.apply(p, args);//. поэтому добавляем к p f.apply(this, p);// вызываем функцию f в контексте this с параметрами p > >;
function a() < var str =""; for(var i=0;ialert(str); > a(1,2,4,5,"six");//1 2 3 4 5 six var b=withVars(a,4,8,15,16,23,42); b(1,2,4,5,"six");//1 2 3 4 5 six 4 8 15 16 23 42
В итоге в нескольких строках кода получаем мощный эффект.
Лёгким движением руки функцию можно переделать таким образом, чтобы параметр добавлялся в начало. Для этого нужно просто заменить
Array.prototype.push.apply(p, args); f.apply(this, p);
Array.prototype.push.apply(args,p); f.apply(this, args);
Возможные применения
- Допустим вам нужно вызвать callback2 из callback1,callback3 из callback2, и т.д., и при этом передать некоторые данные из одного колбека в другой. Мы не можем передать дополнительную информацию в объекте-источнике событий. Например, когда мы используем GM_xmlhttpRequest. Короче, источник событий спрятан от нас, а наружу торчит функция, которую мы можем вызывать, но которая не сохраняет произвольную информацию для передачи в коллбек. Событий будет много и возникать они будут возникать «параллельно», так что создать переменную — не вариант.
function cb1(evt)< . GM_xmlhttpRequest(< method: "GET", url: url2, onload: withVars(cb2,processed2) >); > function cb2(evt)< . GM_xmlhttpRequest(< method: "GET", url: url3, onload: withVars(cb3,processed3) >); > function cb3(evt)< . GM_xmlhttpRequest(< method: "GET", url: url4, onload: withVars(cb4,processed4) >); > function cb3(evt) < . >GM_xmlhttpRequest(< method: "GET", url: url1, onload: cb1 >);
var mult=fstrValidate(function(a,b)< return a*b; >,"%d%d");
Что почитать
UPD1: Добавил несколько применений.
UPD2: немного исправил код
Функции-обёртки, декораторы
Материал на этой странице устарел, поэтому скрыт из оглавления сайта.
Более новая информация по этой теме находится на странице https://learn.javascript.ru/call-apply-decorators.
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( 'Недостаточно прав' ); > >