Под перехватом вызова функции я понимаю возможность динамически задавать блоки кода, которые выполняться до или после перехватываемого вызова функции. Естественно, что такие блоки можно обернуть в функции, которые можно будет использовать многократно для разных перехватываемых вызовов и для разных функций.
Немного об АОП
Идея перехватов вызовов взята из парадигмы аспектно-ориетированного программирования (АОП), но я не хочу вдаваться в дебри этой парадигмы и использовать её терминологию, чтобы ничего не усложнять. Я просто попытаюсь изложить основную идею этой парадигмы.
АОП представляет способ модуляризации (сборки в одном месте) так называемой сквозной, или разбросанной функциональности. Типичный пример такой функциональности это логирование, при котором выводить информацию о том, что происходит в коде, нужно в совершенно разных местах. Очень удобно было бы написать функцию для вывода информации о событиях какого-то типа, но не писать явно её вызов при возникновении события такого типа, а просто сказать, где мы хотим её вызывать. Это и позволяет сделать АОП.
Как это будет выглядеть
Как известно, слова мало чего стоят, поэтому показываю код. Перехватывать будем вызов чего-нибудь простого и всем известного, например, функций, которые производят математические операции над двумя числами.
// Файл functions.js function sum(a, b) { return a + b; } function sub(a, b) { return a - b; } function mult(a, b) { return a * b; } function div(a, b) { return a / b; }
До и после вызова функции будем выводить информацию о её имени, аргументах и возвращаемом результате.
// Файл interceptors.js function printCallInfo(callback, args) { var logStr = "Invoking function '" + callback.name + "' with args: " + args.join(', '); console.log(logStr); } function printCallResult(callback, args, result) { var logStr = "Function '" + callback.name + "' is successfully invoke\nresult: " + result + "\n"; console.log(logStr); }
Теперь создадим перехватчик, который будет хранить информацию о том, что мы хотим делать до перехватываемого вызова функции и после.
// Файл interceptors.js var logInterceptor = new Interceptor(printCallInfo, printCallResult);
Про реализацию Interceptor’a поговорим в конце статьи. Сейчас важнее понять, зачем он вообще нужен, а не как работает.
Скажем, что хотим перехватывать вызовы наших функций.
sum = logInterceptor.interceptInvokes(sum); sub = logInterceptor.interceptInvokes(sub); div = logInterceptor.interceptInvokes(div); mult = logInterceptor.interceptInvokes(mult);
Обратите внимание, что никакого повторяющегося кода в теле функций нет. Просто теперь при их вызовах волшебным образом будет вестись лог. Если нам понадобится изменить его формат или вовсе выключить, сделать это можно, изменив код только в одном месте.
sum(1, 2); sub(3, 2); mult(5, 4); div(20, 4); // turn off logging logInterceptor.preInvoke = function (){}; logInterceptor.postInvoke = function (){}; sum(1, 2); // turn on logging logInterceptor.preInvoke = printCallInfo; logInterceptor.postInvoke = printCallResult; sum(10, 2);
На консоли при выполнении этого скрипта увидим:
Invoking function ‘sum’ with args: 1, 2
Function ‘sum’ is successfully invoke
result: 3
Invoking function ‘sub’ with args: 3, 2
Function ‘sub’ is successfully invoke
result: 1
Invoking function ‘mult’ with args: 5, 4
Function ‘mult’ is successfully invoke
result: 20
Invoking function ‘div’ with args: 20, 4
Function ‘div’ is successfully invoke
result: 5
Invoking function ‘sum’ with args: 10, 2
Function ‘sum’ is successfully invoke
result: 12
Ещё пару примеров
Очень часто внутри функции приходится приводить её аргументы к нужному формату перед тем, как заняться собственно тем, для чего эта функция написана. Не редко конвертировать аргументы в разных функциях нужно к одному и тому же формату. Давайте создадим перехватчик, который будет заниматься приведением значений аргументов наших математических функций к числам.
// Файл interceptors.js var convertToNumberInterceptor = new Interceptor(function (callback, args) { for (var i = 0; i < args.length; i++) { args[i] = parseFloat(args[i]); } });
Используем этот перехватчик для функции сложения.
console.log(sum(3, '2')); // 32 sum = convertToNumberInterceptor.interceptInvokes(sum); console.log(sum(3, '2')); // 5
Если же мы хотим кидать исключение, когда значение аргумента какой-нибудь нашей математической функции не является числом, то для этого тоже можно использовать перехватчик, который может выглядеть как-то так:
// Файл interceptors.js var checkArgsInterceptor = new Interceptor(function (callback, args) { for (var i = 0; i < args.length; i++) { if (typeof args[i] !== 'number') { throw new Error("Argument with index " + i + " is not a number"); } } });
sum = checkArgsInterceptor.interceptInvokes(sum); console.log(sum(1, 2)); // 3 // Uncaught Error: Argument with index 0 is not a number console.log(sum('1', 2));
Реализация
Теперь можно наконец-то посмотреть, как это реализуется. Функция-конструктор перехватчика принимает в качестве параметров функции, которые будут вызваны до и после перехватываемого вызова функции. Оба параметра необязательны, и не переданный или не являющийся функцией параметр будет заменён на пустую функцию.
// Файл interceptor.js function emptyFunction() {}; function Interceptor(preInvoke, postInvoke) { /* * Если preInvoke не функция, то заменяем этот аргумент * на пустую функцию. Вдруг нам не нужно будет ничего делать * до перехватываемого вызова. */ var preInvoke = typeof preInvoke === 'function' ? preInvoke : emptyFunction, // Аналогично с postInvoke postInvoke = typeof postInvoke === 'function' ? postInvoke : emptyFunction; this.preInvoke = preInvoke; this.postInvoke = postInvoke; }
Прототип перехватчика будет содержать единственный метод interceptInvokes, который будет преобразовывать функцию таким образом, чтобы её вызовы перехватывались.
// Файл interceptor.js Interceptor.prototype = { constructor: Interceptor, interceptInvokes: function (callback) { /* * Запоминаем текущий контекст, так как в * в следующей анонимной функции, но уже другой */ var self = this; return function () { // преобразованная функция // конвертируем arguments в массив var args = Array.prototype.slice.call(arguments, 0), /* * Массив с аргументами для preInvoke и postInvoke. * Добавим в него в качестве первого элемента функцию, * вызов которой перехватывается. Вдруг нам понадобится дополнительная информация о ней. */ result; // Делаем что-то до перехватываемого вызова self.preInvoke.call(self, callback, args); result = callback.apply(self, args); // Делаем что-то после self.postInvoke.call(self, callback, args, result); return result; }; } }
Всю реализацию желательно завернуть в анонимную функцию, которую тут же можно вызвать и вернуть в качестве результата объект Interceptor. Таким образом, можно спрятать переменные, необходимые только для локального использования (в нашем случае это только emptyFunction), а так же обеспечить возможность легкого переименования Interceptor‘a.
З.Ы.
Напоследок хочу предостеречь от активного использования этого подхода. Во-первых, из-за того, что он придаёт коду не совсем очевидное поведение. Во-вторых, из-за его чрезмерной ресурсоёмкости, которая является следствием использования медленных методов call и apply. Существуют целые JS АОП-фреймворки, в которых, впрочем, скорее всего, так же будет похожая реализация с использованием тех же медленных call и apply.
На этом всё. Желаю вам успехов!