7

I have a class that I'd like to apply a proxy to, observing method calls and constructor calls:

Calculator.js

class Calc {
  constructor(){}

  add(a, b) {
    return a+b;
  }

  minus(a, b) {
    return a-b;
  }
}

module.exports = Calc;

index.js

const Calculator = require('./src/Calculator');

const CalculatorLogger = {
  construct: function(target, args, newTarget) {
      console.log('Object instantiated');
      return new target(...args);
  },
  apply: function(target, thisArg, argumentsList) {
      console.log('Method called');
  }
}
const LoggedCalculator = new Proxy(Calculator, CalculatorLogger);
const calculator = new LoggedCalculator();
console.log(calculator.add(1,2));

When this is called, I would expect for the output to be:

Object instantiated

Method called

however, the apply is not being called, I assume that this is because I am attaching the Proxy to the Calculator class, but not the instantiated object, and so doesn't know about the apply trap.

How can i build an all encompassing Proxy to "observe" on method calls and constructor calls.

2
  • You're not even applying the calculator object (it's not a function). You're applying the add method. Commented Jun 13, 2018 at 17:31
  • 1
    FYI, you should always try to use Reflect where possible in proxy handlers, so here you'd want Reflect.construct rather than new target. Commented Jun 13, 2018 at 17:34

2 Answers 2

5

I assume that this is because I am attaching the Proxy to the Calculator class, but not the instantiated object, and so doesn't know about the apply trap.

You are totally right, proxies act upon objects, so it won't call apply unless a function property of the Calculator class is called, as follows:

class Calculator {
  constructor() {
    this.x = 1;
  }

  instanceFunction() {
    console.log('Instance function called');
  }

  static staticFun() {
    console.log('Static Function called');
  }

}

const calcHandler = {
  construct(target, args) {
    console.log('Calculator constructor called');
    return new target(...args);
  },
  apply: function(target, thisArg, argumentsList) {
    console.log('Function called');
    return target(...argumentsList);
  }
};

Calculator = new Proxy(Calculator, calcHandler);

Calculator.staticFun();

const obj = new Calculator();

obj.instanceFunction();

With that clear, what you could do to wrap an instance of Calculator with a proxy could be:

  1. Have the class proxy to proxify instances on construct:

const CalculatorInstanceHandler = {
  apply(target, thisArg, args) {
    console.log('Function called');
    return target(...args);
  }
}

const CalculatorClassHandler = {
  construct(target, args) {
    const instance = new target(...args);
    return new Proxy(instance, CalculatorInstanceHandler);
  }
}

  1. Have a factory function in the Calculator class in order to create proxified instances:

const CalculatorInstanceHandler = {
  apply(target, thisArg, args) {
    return target(...args);
  }
};

class Calculator {

  static getNewCalculator() {
    const instance = new Calculator();

    return new Proxy(instance, CalculatorInstanceHandler);

  }
}

Sign up to request clarification or add additional context in comments.

Comments

2

Instead of using handler.apply() on the class, modify what handler.construct() returns, adding a Proxy to that instead.

class originalClass {
  constructor() {
    this.c = 1;
  }
  add(a, b) {
    return a + b + this.c;
  }
}
const proxiedClass = new Proxy(originalClass, {
  construct(target, args) {
    console.log("constructor of originalClass called.");
    return new Proxy(new target(...args), {
      get(target, prop, receiver) {
        console.log(prop + " accessed on an instance of originalClass");
        const val = target[prop];
        if (typeof target[prop] === "function") {
          console.log(prop + " was a function");
          return function(...args) {
            console.log(prop + "() called");
            return val.apply(this, args);
          };
        } else {
          return val;
        }
      }
    });
  }
});
const proxiedInstance = new proxiedClass();
console.log(proxiedInstance.add(1, 2));

There's 2 proxies in play here:

  • A proxy to observe constructor calls, and wrap any instances created by that constructor with...
  • ...a proxy to observe property accesses, and log when those properties are functions. It will also wrap any functions, so it can observe calls to that function.

3 Comments

Notice that accessing a function-valued property is not necessarily the same as calling it
Thanks, @Bergi. I've edited the answer to wrap the function.
@uber5001 This is a great answer, but you probably should update it with these improvements: 1) use Reflect.construct instead of new target 2) when wrapping the functions, call val.apply(target, args) - this will allow the function call to reference the original object instead of the proxy. Otherwise the objects implementation will continue to pass through the proxy on every this dereference. this may need to be an option depending on what is required for the task.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.