1

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
  • 1
    yes, a Proxy may be able to do that - have you tried?
    – Bravo
    Commented Aug 18, 2021 at 12:28
  • Or any other kind of wrapper, just another object with the same methods that acts as a proxy for example.
    – jonrsharpe
    Commented Aug 18, 2021 at 12:29
  • VTR: Neither the question nor the answers in the linked question really matches this use-case. Commented Aug 18, 2021 at 18:36
  • @ScottSauyet What use case? The use case was not specified. OP gave generic pseudo code. Commented Aug 18, 2021 at 18:53
  • @Iwrestledabearonce: just the notion of wrapping object methods. The linked question was about extending the built-in reference types. While they have some overlap in the possibilities using Proxies, the two questions don't seem to be a close fit for one another. Commented Aug 18, 2021 at 18:57

5 Answers 5

1

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
  • 1
    1/3 ... Of cause one can go with this approach. One just has to keep in mind that one has created a trap for getting any property value. Thus the handler in addition needs to test the typeof oTarget[sKey] being a 'function' type. Otherwise one would use the call/() operator on any other, but not callable, type too like when trying to e.g. get foo of obj.foo which was probably set before via obj.foo = 'Foo'. In such a case the handler needs to return oTarget[sKey] as is. Commented Aug 18, 2021 at 16:12
  • 1
    2/3 ... One also needs to keep in mind that the above handler, if implemented with the correct function-type checking, does create and return another function with every valid trapping get, regardless if the function then is going to be (immediately) invoked or not. One also should notice that the above implementation does target any of an object's method and does handle them all the same. If one wants to target a specific method, like obj.doB one has to provide such a sKey specific logic within the proxy handler too. Commented Aug 18, 2021 at 16:12
  • 1
    3/3 ... Last, since one does proxify an object and does target method calls on such an object one needs to change oTarget[sKey](...arguments) to oTarget[sKey].apply(oTarget, args) Commented Aug 18, 2021 at 16:14
  • 1
    Thanks a lot @PeterSeliger. I have updated the code with your suggestions. Commented Aug 18, 2021 at 16:25
1

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();

4
  • The currently presented wrapper solution does not take any this context and arguments handling into account which is crucial for any method call / invocation. Commented Aug 18, 2021 at 13:37
  • @PeterSeliger I've added support for this and arguments, despite none of that being part of the OP, and despite that fact that the second half of your complaint is patently false. Your nitpicking is not adding any value to this question. I closed the question anyway. Commented Aug 18, 2021 at 18:04
  • Thanks for the update (hence the upvote). And my 1st comment was not just for the sake of nitpicking. It is about being precise and knowing ahead the next stumbling block a less experienced developer will trip over. One could save the OP in advance from that by providing a good/viable enough example code and explanation at first place. Commented Aug 18, 2021 at 18:49
  • ... and I reopened it, as the suggested duplicate did not seem a propos. Peter's comments are pretty spot-on if we're trying to write a very generic method wrapper, but it's not at all clear from the question if that's so. I took an intermediate approach that handled arguments, but left this aside... maybe because I rarely use this myself. Commented Aug 18, 2021 at 18:50
1

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.

1

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; }

3
  • While there's lots of good stuff here, it might be worth investigating how much this has to do with the OP's problem. If the answer is "not much", then this might be better suited as a developer's article posted somewhere than in a Q & A site. Commented Aug 18, 2021 at 18:54
  • @ScottSauyet ... I like the highly advanced skill of addressing critique in the least painful way possible. I know, some consider the above answer too much. But for me its always worth it. Not every questioner, despite having asked not the best SO conform way or because of a low reputation, is automatically not (cap)able to comprehend a more in-depth answer. And not always does an answer need to target the OP alone since there are other readers as well that got intrigued on/at the topic and/or question. Commented Aug 18, 2021 at 19:19
  • Well, I meant it about "investigating". None of us responding has really probed the OP to find the true requirements. I try a middle-ground between @Iwrestledabearonce.'s minimalism and your more thorough treatment. But it's not clear if even that is far too much. If all the OP is looking for is to do a console .log after completing the main body, there are trivial solutions. But it could also go where you do and even further. In any case, regardless of the result here, I would love to see this posted as a blog post, on medium, dev.to, or some such. Commented Aug 18, 2021 at 19:36
0

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!
1
  • 1
    This approach does rely on changing the original implementation of a method. What the OP did not mention, but where the OP's pain might come from is, that the OP can not touch / is not allowed to touch this kind of closed code. Commented Aug 18, 2021 at 13:41

Not the answer you're looking for? Browse other questions tagged or ask your own question.