Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Javascript: know which object function has been called

I would like to know if there is a way to be notified when any object function is called, in javascript.

For example, I would like to do something like the following:

If I have an object like this:

myObject = {
    functionOne: function(argumentOne) {
        // do some stuff
    },

    functionTwo: function() {
        // do some stuff
    }
}

And add a listener (or anything else to track the actions taken place on this object):

myObject.addEventListener('onFunctionCall', functionHasBeenCalled);

When I call:

myObject.functionOne('hello');

The listener handler to fire with information about the called function:

functionHasBeenCalled(calledFunctionData) {
    console.log(calledFunctionData.functionName + ' has been called');
    console.log('with argument: ' + calledFunctionData.functionArgument);
}

And the console output to be:

functionOne has been called
with argument: hello

Maybe there is another way, to implement this, not with an event listener, but I have no clue.

Thanks!

like image 881
ealef Avatar asked Jan 27 '23 16:01

ealef


2 Answers

One approach could be to use a Proxy to create "tracked" objects where you intercept any method invocations:

function trackMethodCalls(obj) {
  const handler = {
    // anytime we do obj.someMethod
    // we actually return the interceptedMethod instead
    get(target, propKey, receiver) {
      
      const method = target[propKey];

      // we only do something special if we're working with a function
      // on the object. If the property isn't a function we can just return
      // it as normal.
      if (typeof method !== 'function') {
        return method;
      }

      return function interceptedMethod(...args) {
        const result = method.apply(this, args);

        console.log(
          `${propKey}(${args.join(",")}) = ${JSON.stringify(result)}`
        );

        return result;
      };
    }
  };
  return new Proxy(obj, handler);
}

const obj = {
  val: 2,
  double(x) {
    return this.val * x;
  }
};

const trackedObj = trackMethodCalls(obj);

trackedObj.double(4);

If you want to mutate an object rather than instrumenting it via a Proxy, you should just directly overwrite its methods:

function addTrackedMethods(obj) {
  for (const [methodName, method] of Object.entries(obj).filter(
    ([, method]) => typeof method === "function"
  )) {
    obj[methodName] = function interceptedMethod(...args) {
      const result = method.apply(this, args);

      console.log(
        `${methodName}(${args.join(",")}) = ${JSON.stringify(result)}`
      );

      return result;
    };
  }
}

const obj = {
  val: 2,
  double(x) {
    return this.val * x;
  }
};

addTrackedMethods(obj);

obj.double(4);
like image 134
nem035 Avatar answered Jan 29 '23 04:01

nem035


You can't do that without getting between the object and the thing calling it, or modifying the object. There isn't any "tap into this interaction" that doesn't involve one or the other.

Here's each:

Getting between them

If you can get between the object and the thing calling it, you can create a new object to do that, with functions duplicating the functions on the target object that call it. Here's a simple example:

const original = {
    value: 42,
    doSomething() {
        console.log(`original doSomething: this.value is ${this.value}`);
    },
    doSomethingElse() {
        console.log(`original doSomethingElse: this.value is ${this.value}`);
    }
};

const insertion = {};
for (const key of Object.keys(original)) {
    const func = original[key];
    if (typeof func === "function") {
        insertion[key] = function(...args) {
            console.log("insertion " + key + " [before]");
            const thisArg = this === insertion ? original : this;
            const result = Reflect.apply(func, thisArg, args);
            console.log("insertion " + key + " [after]");
            return result;
        };
    }
}

// Here's the code calling the object's methods;
// note we've gotten in the way by giving the code
// `insertion` rather than `original`:
insertion.doSomething();
insertion.doSomethingElse();

You can also do that with a Proxy, though it's more complicated and the complication doesn't buy you anything in this case.

Note that this will only catch calls make through insertion, for obvious reasons. That means if doSomething calls doSomethingElse, in the above you'd intercept the call to doSomething but not the call to doSomethingElse.

Modifying the object

You can do it by replacing the object's methods, like this:

const original = {
    value: 42,
    doSomething() {
        console.log(`original doSomething: this.value is ${this.value}`);
    },
    doSomethingElse() {
        console.log(`original doSomethingElse: this.value is ${this.value}`);
    }
};

for (const key of Object.keys(original)) {
    const func = original[key];
    if (typeof func === "function") {
        original[key] = function(...args) {
            console.log(key + " [before]");
            const result = Reflect.apply(func, this, args);
            console.log(key + " [after]");
            return result;
        };
    }
}

// Here's the code calling the object's methods
original.doSomething();
original.doSomethingElse();

Since this modifies the object itself, you'd see all the calls.

like image 38
T.J. Crowder Avatar answered Jan 29 '23 06:01

T.J. Crowder