3

I want to insert on pre execute and post execute hooks on functions in javascript classes.

Lets say I have a class like this.

class Foo {
  method1(p1, p2) {
    this.p1 = p1;
    this.p2 = p2;
  }

  method2(p3) {
    this.p3 = p3;
  }
}

I want to define a before and after hook for these preexisting class methods. Something like this.

class Foo {
  before(funName, ...params){
    // Should print ('method1', [p1, p2]) when method 1 is called
    // and ('method2', [p3]) when method 2 is called
    console.log(funName, params)
  }
  after(funName, result){
    // Should print the function name followed by its result
    console.log(funName, result)
  }
  method1(p1, p2) {
    this.p1 = p1;
    this.p2 = p2;
  }
  method2(p3) {
    this.p3 = p3;
  }
}

export default Foo;

What is the best way of implementing these hooks with minimal changes in existing code?

Souradeep Nanda
  • 3,116
  • 2
  • 30
  • 44

2 Answers2

5

Here is a rough solution to the problem:

// we iterate over all method names
Object.getOwnPropertyNames(Foo.prototype).forEach((name) => {

  // First to do: we save the original method. Adding it to prototype
  // is a good idea, we keep 'method1' as '_method1' and so on
  Foo.prototype['_' + name] = Foo.prototype[name];

  // Next, we replace the original method with one that does the logging
  // before and after method execution. 
  Foo.prototype[name] = function() {

    // all arguments that the method receives are in the 'arguments' object
    console.log(`Method call: method1(${Object.values(arguments).join(', ')})`);

    // now we call the original method, _method1, on this with all arguments we received
    // this is probably the most confusing line of code here ;)
    // (I never user this['method'] before - but it works)
    const result = this['_' + name](...arguments);

    // here is the post-execution logging
    console.log(`Method result: ${result}`);

    // and we need to return the original result of the method
    return result;
  };
});

Please note that this code is not part of the class itself, execute it as a normal script.

And there is a good chance that this short proof of concept crashes on real-world classes and requires some additional checks and special-case handlers, especially to get proper logging output. But it works with you Foo class.

Here's the working example: https://codesandbox.io/s/great-fog-c803c

Andreas Dolk
  • 113,398
  • 19
  • 180
  • 268
  • And Proxies would seem to be relevant? https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy – Ben Aston Feb 23 '20 at 22:09
  • 1
    I tried to find a solution with proxies, as it was my first idea. But I couldn't find a way to proxy a class method that uses `this`. `this` was always undefined and I didn't figure out how to 'auto-bind' the method proxy to the class instance. If someone has a solution for that, I'd be happy to see that too. – Andreas Dolk Feb 23 '20 at 22:30
0

Proxy solution

class Foo {
  classAlias = false;

  proxyHandle = {
    // little hack where we save refrenece to our class inside the object
    main : this,
    /**
     * The apply will be fired each time the function is called
     * @param  target Called function
     * @param  scope  Scope from where function was called
     * @param  args   Arguments passed to function
     * @return        Results of the function
     */
    apply : function (target, scope, args) {
      const func_name = target.name;

      console.log('before', func_name);

      // let's call some method of this class to actually check if this is the right instance
      // also remember that you have to exclude methods which you are gonna use
      // inside here to avoid “too much recursion” error
      this.main._free.instanceCheck();


      // here we bind method with our class by accessing reference to instance
      const results = target.bind(this.main)(...args);

      console.log('after', func_name, results);
      return results;
    }
  }

  constructor(classAlias) {
    // Get all methods of choosen class
    let methods = Object.getOwnPropertyNames( Foo.prototype );

    // Find and remove constructor as we don't need Proxy on it
    let consIndex = methods.indexOf('constructor');
    if ( consIndex > -1 ) methods.splice(consIndex, 1);

    // Replace all methods with Proxy methods
    methods.forEach( methodName => {
      this[methodName] = new Proxy( this[methodName], this.proxyHandle );
    });
    this.classAlias = classAlias;
  }

  // easies trick to do to avoid infinite loop inside apply is to set your functions
  // inside object, as getOwnPropertyNames from prototype will only get methods
  _free = {
    instanceCheck : () => {
      // this will check if this is our Foo class
      console.log(this.classAlias);
    }
  }

  log() {
    return 'Result';
  }
}

(new Foo('Proxy Class A')).log();

/* 
  Output:
    before log
    Proxy Class A
    after log Result
*/

Just wanted to share because I've read in comments that someone had problem to set Proxy. You can read more about Proxies here, and here more about apply.

Remember that in proxy handle, the this is actually this.main. For better understanding, you can change it to classInstance or something similar.

Mortimer
  • 300
  • 5
  • 11