Перехват вызова функции в JavaScript

Под перехватом вызова функции я понимаю возможность динамически задавать блоки кода, которые выполняться до или после перехватываемого вызова функции. Естественно, что такие блоки можно обернуть в функции, которые можно будет использовать многократно для разных перехватываемых вызовов и для разных функций.

Немного об АОП

Идея перехватов вызовов взята из парадигмы аспектно-ориетированного программирования (АОП), но я не хочу вдаваться в дебри этой парадигмы и использовать её терминологию, чтобы ничего не усложнять. Я просто попытаюсь изложить основную идею этой парадигмы.

АОП представляет способ модуляризации (сборки в одном месте) так называемой сквозной, или разбросанной функциональности. Типичный пример такой функциональности это логирование, при котором выводить информацию о том, что происходит в коде, нужно в совершенно разных местах. Очень удобно было бы написать функцию для вывода информации о событиях какого-то типа, но не писать явно её вызов при возникновении события такого типа, а просто сказать, где мы хотим её вызывать. Это и позволяет сделать АОП.

Как это будет выглядеть

Как известно, слова мало чего стоят, поэтому показываю код. Перехватывать будем вызов чего-нибудь простого и всем известного, например, функций, которые производят математические операции над двумя числами.

// Файл 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.

На этом всё. Желаю вам успехов!

Скачать исходники (1.6 Кб)