What are ways of intercepting function calls or changing a function's behavior?

827 Views Asked by At

I would like to execute some code every time some functions in an object are called and finish executing.

Object:

{
    doA() {
        // Does A
    },
    doB() {
        // Does B
    }
}

Is it possible to extend it, changing those functions so that they will do what they do and after do something else? Like it was an event listening for those functions finishing?

{
    doA() {
        // Does A
        // Do something else at end
    },
    doB() {
        // Does B
        // Do something else at end
    }
}

Maybe this would be possible using Proxy https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy

Tried with proxy:

const ob = {
    doA() {
        console.log('a');
    },
    doB() {
        console.log('b');
    }
};

const ob2 = new Proxy(ob, {
  apply: function (target, key, value) {
    console.log('c');
  },
});

ob2.doA();
5

There are 5 best solutions below

4
user5507535 On BEST ANSWER

Using Proxy we can target all get which includes functions, then we can check if what is being get is a function and if it is we create and return our own function that wraps the object function and call it.

After the object function is called from within our wrapper function we can execute whatever we want and then return the return value of the object function.

const ob = {
  doA(arg1, arg2) {
    console.log(arg1, arg2);
    return 1;
  },
  doB() {
    console.log('b');
  }
};

const ob2 = new Proxy(ob, {
  get: function(oTarget, sKey) {
    if (typeof oTarget[sKey] !== 'function')    return oTarget[sKey];

    return function(...args) {
      const ret = oTarget[sKey].apply(oTarget, args);

      console.log("c");

      return ret;
    }
  }
});

console.log(ob2.doA('aaa', 'bbb'));

If there are improvements or other options please add a comment!

4
I wrestled a bear once. On

Here's an example of one straight-forward way to wrap a function in an object.

This can be useful for debugging, but you should not do this in production because you (or anyone looking at your code later on) will have a difficult time figuring out why your method is doing things that are not in the original code.

var myObj = {
  helloWorld(){
    console.log('Hello, world!');
  }
}


// get a refernce to the original function
var f = myObj.helloWorld;

// overwrite the original function
myObj.helloWorld = function(...args){
  
  // call the original function first
  f.call(this, ...args);
  
  // Then  do other stuff afterwards
  console.log('Goodbye, cruel world..');
};


myObj.helloWorld();

1
Colin On

If it's about having the same mechanism of event listeners you could create an object that con store functions and execute them whenever you want

const emitter =  {
    events: {},    

    addListener(event, listener) {
        this.events[event] = this.events[event] || [];
        this.events[event].push(listener);
    },

    emit(event, data) {
        if(this.events[event]) {
            this.events[event].forEach(listener => listener(data));
        }
    }
}

//instead of the config object you could just type the string for the event
const config = {
    doA: 'doA',
    doB: 'doB'
}

//store first function for doA
emitter.addListener(config.doA, (data) => {
    console.log('hardler for Function ' + data + ' executed!');
});

//store second function for doA
emitter.addListener(config.doA, () => {
    console.log('Another hardler for Function A executed!');
});

//store first function for doB
emitter.addListener(config.doB, (data) => {
    console.log('hardler for Function ' + data + ' executed!');
});

let obj = {
    doA() {
        let char = 'A';
        console.log('doA executed!');
        //You can pass data to the listener
        emitter.emit(config.doA, char);
    },

    doB() {
        let char = 'B';
        console.log('doB executed!');
        emitter.emit(config.doB, char);
    }
}

obj.doA();
obj.doB();

//Output:
//doA executed!
//hardler for Function A executed!
//Another hardler for Function A executed!
//doB executed!
//hardler for Function B executed!
3
Peter Seliger On

With JavaScript applications one sometimes is in need of intercepting and/or modifying the control flow of functionality one does not own or is, for other reasons, not allowed to touch.

For exactly this scenario there is no other way than to preserve and alter such logic by wrapping their original implementation. This ability is not unique to JavaScript. There is quite a history of programming languages that enable Metaprogramming via Reflection and Self-Modification.

Of cause one could/should provide bulletproof but handy abstractions for all the possible modifier use cases which one can think of.

Since JavaScript already does implement Function.prototype.bind which already comes with some kind of tiny modifying capability, I personally wouldn't mind if, at one day, JavaScript officially features the tailored and standardized handy method-modifier toolset of ... Function.prototype[before|around|after|afterThrowing|afterFinally].

// begin :: closed code
const obj = {
  valueOf() {
    return { foo: this.foo, bar: this.bar };
  },
  toString(link = '-') {
    return [this.foo, this.bar].join(link);
  },
  foo: 'Foo',
  bar: 'Bar',
  baz: 'BAAAZZ'
};
// end :: closed code

console.log(
  'obj.valueOf() ...',
  obj.valueOf()
);
console.log(
  'obj.toString() ...',
  obj.toString()
);


enableMethodModifierPrototypes();


function concatBazAdditionally(proceed, handler, [ link ]) {
  const result = proceed.call(this, link);
  return `${ result }${ link }${ this.baz }`;
}
obj.toString = obj.toString.around(concatBazAdditionally, obj);
// obj.toString = aroundModifier(obj.toString, concatBazAdditionally, obj)

console.log(
  '`around` modified ... obj.toString("--") ...',
  obj.toString("--")
);


function logWithResult(result, args) {
  console.log({ modifyerLog: { result, args, target: this.valueOf() } });
}
obj.toString = obj.toString.after(logWithResult, obj);
// obj.toString = afterModifier(obj.toString, logWithResult, obj)

console.log(
  '`around` and `after` modified ... obj.toString("##") ...',
  obj.toString("##")
);


function logAheadOfInvocation(args) {
  console.log({ stats: { args, target: this } });
}
obj.valueOf = obj.valueOf.before(logAheadOfInvocation, obj);
// obj.valueOf = beforeModifier(obj.valueOf, logAheadOfInvocation, obj)

console.log(
  '`before` modified ... obj.valueOf() ...',
  obj.valueOf()
);


restoreDefaultFunctionPrototype();
.as-console-wrapper { min-height: 100%!important; top: 0; }
<script>
  function isFunction(value) {
    return (
      typeof value === 'function' &&
      typeof value.call === 'function' &&
      typeof value.apply === 'function'
    );
  }

  function getSanitizedTarget(value) {
    return value ?? null;
  }

  function around(handler, target) {
    target = getSanitizedTarget(target);

    const proceed = this;
    return (
      isFunction(handler) &&
      isFunction(proceed) &&

      function aroundType(...args) {
        const context = getSanitizedTarget(this) ?? target;

        return handler.call(context, proceed, handler, args);
      }
    ) || proceed;
  }
  around.toString = () => 'around() { [native code] }';

  function before(handler, target) {
    target = getSanitizedTarget(target);

    const proceed = this;
    return (
      isFunction(handler) &&
      isFunction(proceed) &&

      function beforeType(...args) {
        const context = getSanitizedTarget(this) ?? target;

        handler.call(context, [...args]);

        return proceed.apply(context, args);
      }
    ) || proceed;
  }
  before.toString = () => 'before() { [native code] }';

  function after(handler, target) {
    target = getSanitizedTarget(target);

    const proceed = this;
    return (
      isFunction(handler) &&
      isFunction(proceed) &&

      function afterReturningType(...args) {
        const context = getSanitizedTarget(this) ?? target;
        const result = proceed.apply(context, args);

        handler.call(context, result, args);

        return result;
      }
    ) || proceed;
  }
  after.toString = () => 'after() { [native code] }';

  function aroundModifier(proceed, handler, target) {
    return around.call(proceed, handler, target);
  }
  function beforeModifier(proceed, handler, target) {
    return before.call(proceed, handler, target);
  }
  function afterModifier(proceed, handler, target) {
    return after.call(proceed, handler, target);
  }

  const { prototype: fctPrototype } = Function;

  const methodIndex = {
    around,
    before,
    after/*Returning*/,
    // afterThrowing,
    // afterFinally,
  };
  const methodNameList = Reflect.ownKeys(methodIndex);

  function restoreDefaultFunctionPrototype() {
    methodNameList.forEach(methodName =>
      Reflect.deleteProperty(fctPrototype, methodName),
    );
  }
  function enableMethodModifierPrototypes() {
    methodNameList.forEach(methodName =>
      Reflect.defineProperty(fctPrototype, methodName, {
        configurable: true,
        writable: true,
        value: methodIndex[methodName],
      }),
    );
  }
</script>

<!--
<script src="https://closure-compiler.appspot.com/code/jscd16735554a0120b563ae21e9375a849d/default.js"></script>
<script>
  const {

    disablePrototypes: restoreDefaultFunctionPrototype,
    enablePrototypes: enableMethodModifierPrototypes,
    beforeModifier,
    aroundModifier,
    afterModifier,

  } = modifiers;
</script>
//-->

The next provided example code uses the above test object and its test cases but implements/provides a proxy based solution. From how the test cases need to be adapted, one can see that direct method modification, based on a clean implementation of method-modifiers, allows a more flexible handling of different use cases, whereas the proxy based approach is limited to one handler function per intercepted method call ...

// begin :: closed code
const obj = {
  valueOf() {
    return { foo: this.foo, bar: this.bar };
  },
  toString(link = '-') {
    return [this.foo, this.bar].join(link);
  },
  sayHi() {
    console.log('Hi');
  },
  foo: 'Foo',
  bar: 'Bar',
  baz: 'BAAAZZ'
};
// end :: closed code

console.log(
  'non proxy call ... obj.valueOf() ...',
  obj.valueOf()
);
console.log(
  'non proxy call ... obj.toString() ...',
  obj.toString()
);


function toStringInterceptor(...args) {
  const { proceed, target } = this;
  const [ link ] = args;

  // retrieve the original return value.
  let result = proceed.call(target, link);

  // modify the return value while
  // intercepting the original method call.
  result = `${ result }${ link }${ target.baz }`;

  // log before ...
  console.log({ toStringInterceptorLog: { result, args, target: target.valueOf() } });

  // ... returning the
  // modified value.
  return result;
}

function valueOfInterceptor(...args) {
  const { proceed, target } = this;

  // log before returning ...
  console.log({ valueOfInterceptorLog: { proceed, args, target } });

  // ... and save/keep the
  // original return value.
  return proceed.call(target);
}

function handleTrappedGet(target, key) {
  const interceptors = {
    toString: toStringInterceptor,
    valueOf: valueOfInterceptor,
  }
  const value = target[key];

  return (typeof value === 'function') && (

    interceptors[key]
      ? interceptors[key].bind({ proceed: value, target })
      : value.bind(target)

  ) || value;
}
const objProxy = new Proxy(obj, { get: handleTrappedGet });

console.log('\n+++ proxy `get` handling +++\n\n');

const { foo, bar, baz } = objProxy;
console.log(
  'non method `get` handling ...',
  { foo, bar, baz }
);
console.log('\nproxy call ... objProxy.sayHi() ... but not intercepted ...');
objProxy.sayHi();

console.log('\nintercepted proxy calls ...');
console.log(
  'objProxy.toString("--") ...',
  objProxy.toString("--")
);
console.log(
  'objProxy.valueOf() ...',
  objProxy.valueOf()
);
.as-console-wrapper { min-height: 100%!important; top: 0; }

0
Scott Sauyet On

You could certainly do this with a Proxy. But you can also write your own generic function decorator to do this.

The basic decorator might work like this:

const wrap = (wrapper) => (fn) => (...args) => {
  (wrapper .before || (() => {})) (...args)
  const res = fn (...args)
  const newRes = (wrapper .after || (() => {})) (res, ...args)
  return newRes === undefined ? res : newRes
}

const plus = (a, b) => a + b

const plusPlus = wrap ({
  before: (...args) => console .log (`Arguments: ${JSON.stringify(args)}`),
  after: (res, ...args) => console .log (`Results: ${JSON.stringify(res)}`)
}) (plus)

console .log (plusPlus (5, 7))

We supply optional functions to run before the main body (with the same parameters) and after it (with the result as well as the initial parameters), and pass to the resulting function the function we want to decorate. The generated function will call before, the main function, and then after, skipping them if they're not supplied.

To wrap the elements of your object using this, we can write a thin wrapper that handles all functions:

const wrap = (wrapper) => (fn) => (...args) => {
  (wrapper .before || (() => {})) (...args)
  const res = fn (...args)
  const newRes = (wrapper .after || (() => {})) (res, ...args)
  return newRes === undefined ? res : newRes
}

const wrapAll = (wrapper) => (o) => Object .fromEntries (
  Object .entries (o) .map (([k, v]) => [k, typeof v == 'function' ? wrap (wrapper) (v) : v])
)

const o = {
    doA () {
        console .log ('Does A')
    },
    doB () {
        console .log ('Does B')
    }
}

const newO = wrapAll ({
  after: () => console .log ('Does something else at end')
}) (o)

newO .doA ()
newO .doB ()

Of course this could be extended in multiple ways. We might want to choose the specific function properties to wrap. We might want to handle this fluently. We might want before to be able to alter the parameters passed to the main function. We might want to give the generated function a useful name. Etc. But it's hard to design the signature for a generic wrapper than can do all those things easily.