Search code examples
javascriptoopproxy-classes

Proxy usage messes up function.caller.name


I am trying to mimic a more class-like inheritance model using JavaScript, but I hit a problem when trying to mix this with the idea of JavaScript Proxies.

To make a long story short, in the definition of my Class type I have a function _super() with the semantics "when method X on an instance of subclass B invokes _super(), call method X on parent class A":

Class A
   .X() {...}
   ^
   |
   |
Class B
   .X() {..._super(); ...}

I am relying on the function.caller.name approach to get me the name of the invoking method (in our example, "X"). Then I call it on the parent class.

const Class = {
...
    _super: function _super(...args) {
      // Get a handle on the function in which this function is invoked:
      const callerMethod = _super.caller.name;
      ...
    },
...
};

This works correctly. The problems started when I added a Proxy object on top of my Class construct (I want to trap some method calls).

function traceMethodCalls(obj) {
  const handler = {
    get(target, propKey, receiver) {
      const origMethod = target[propKey];
      return function (...args) {
        // Do stuff
      };
    },
  };
  return new Proxy(obj, handler);
}

Now, function.caller in the _super() method is the anonymous function in the Proxy handler object (obviously...), and this messes up the program flow.

My question: is there a way to circumvent this? Or think about it differently? Or do I have to give up on the *.caller.name approach altogether?


Solution

  • The only thing that comes into mind is to inspect the stack to find the first thing that isn't "_super". Quite silly IMO, but here it goes.

    const Class = {
    
        _super: function _super(...args) {
            let callerMethod;
    
            let s = (new Error)
                .stack.split('\n')
                .slice(2);
            while (s.length && s[0].includes('_super'))
                s.shift();
    
            let m = (s[0] || '').match(/^\s*at\s\w+\.(\w+)/);
            callerMethod = m ? m[1] : null;
            console.log('super call [%s]', callerMethod)
        },
    
        foo: function () {
            this._super()
        }
    };
    
    
    function traceMethodCalls(obj) {
        const handler = {
            get(target, propKey, receiver) {
                const origMethod = target[propKey];
                let f = {
                    [propKey]: function (...args) {
                        console.log('tracing', propKey)
                        origMethod.bind(this)()
                    }
                };
                return f[propKey];
            },
        };
        return new Proxy(obj, handler);
    }
    
    obj = Object.create(Class)
    obj.foo()
    traced = traceMethodCalls(obj)
    traced.foo()

    In general, to rely on function names in always dangerous (think uglifiers et al). I guess it would be fair to say that you can't get bare super working in js without some kind of precompilation e.g. annotations.