Can I extend default javascript function prototype to let some code been executed on every function call?

113 views Asked by At

Lets say there are functions

function a(someparams){
 console.log('a called')
}
function b(){
 console.log('b called')
}
...
const c (someParam) => { console.log('c called')}

I want to extend default function prototype something like Function.prototype.onCall = (args) => {console.log('Proxy fn called!',args)} in a very begining of a page code, so every existing and new functions, upon call, will log 'Proxy fn called!'. There are solutions to map window.functions - but they only work for existing functions, and I want to extend prototype to let it execute my piece of code. The goal is to automatically check if args are valid. No I don't want typescript or flow, thanks for suggestion. Is this possible? where to look at?

I found

(function() {
    var call = Function.prototype.call;
    Function.prototype.call = function() {
        console.log(this, arguments); // Here you can do whatever actions you want
        return call.apply(this, arguments);
    };
}());

to be the closest to what I want so far, but these aren't called when calling a function normally, e.g. someFunction();, without explicit .call or .apply.

I have found maybe decorators is a way to go? the official doc said they are still not a standard https://github.com/tc39/proposal-decorators but there is a babel plugin https://babeljs.io/docs/en/babel-plugin-proposal-decorators is it possible that it would do the job or am I looking into wrong direction?

1

There are 1 answers

3
Peter Seliger On BEST ANSWER

Proposal

What the OP is looking for is best described with method modification. There are specialized modifiers like around, before, after, afterThrowing and afterFinally.

In case of a search for modifier implementations one should be aware that such a modifier has to support a thisArg parameter in order to create a modified function which operates upon the correct this context (hence a method).

Answer

There is no single point where one could implement the interception of any function's invocation, be it existing functions or the ones yet to come (e.g. functions/methods generated while an application is running).

Regardless of the possible approaches like a Proxy's apply or construct method's or decorators (as proposed by another answer) or the just mentioned method modifiers (which are specialized abstractions for otherwise more complex manual wrapping tasks), one always has to know and explicitly refer to the to be proxyfied / decorated / modified functions and methods.

Example code which implements and uses Function.prototype.around ...

// 1st example ... wrapping **around** a simple function

function consumeAnyParameter(...params) {
  console.log('inside `consumeAnyParameter` ... params ...', params);
}
function interceptor(originalFunction, interceptorReference, ...params) {
  // intercept.
  console.log('inside `interceptor` ... ', {
    params,
    originalFunction,
    interceptorReference,
  });
  // proceed.
  originalFunction(...params);
}

// reassignment of ... a modified version of itself.
consumeAnyParameter = consumeAnyParameter.around(interceptor);

// invoke modified version.
consumeAnyParameter('foo', 'bar', 'baz');


// 2nd example ... wrapping **around** a type's method.

const type = {
  foo: 'foo',
  bar: 'bar',
  whoAmI: function () {
    console.log('Who am I? I\'m `this` ...', this);
  }
}
type.whoAmI();

type.whoAmI = type
  .whoAmI
  .around(function (proceed, interceptor, ...params) {

    console.log('interceptor\'s `this` context ...', this);
    console.log('interceptor\'s `params` ...', params);

    // proceed.apply(this, params);
    proceed.call(this);

  }, type); // for method modification do pass the context/target. 

type.whoAmI();
.as-console-wrapper { min-height: 100%!important; top: 0; }
<script>
// poor man's module.
(function (Function) {

  // module scope.

  function getSanitizedTarget(value) {
    return value ?? null;
  }
  function isFunction(value) {
    return (
      'function' === typeof value &&
      'function' === typeof value.call &&
      'function' === typeof value.apply
    );
  }

  // modifier implementation.

  function around/*Modifier*/(handler, target) {
    target = getSanitizedTarget(target);

    const proceed = this;

    return (
      isFunction(handler) &&
      isFunction(proceed) &&

      function aroundType(...argumentArray) {
        // the target/context of the initial modifier/modification time
        // still can be overruled by a handler's apply/call time context.
        const context = getSanitizedTarget(this) ?? target;

        return handler.call(
          context,
          proceed,
          handler,
          argumentArray,
        );
      }
    ) || proceed;
  }

  // modifier assignment.

  Object.defineProperty(Function.prototype, 'around', {
    configurable: true,
    writable: true,
    value: around/*Modifier*/,
  });

}(Function));
</script>