0

I can't figure out why the apply trap isn't being called here:

let p1 = new Proxy(Date, {
    apply: function(target, that, args) {
        console.log(`function called ${target.name}`);
        return target.apply(args);
    }
});
console.log(`p1 descriptors: ${Object.getOwnPropertyNames(p1)}`);
console.log(`p1.now(): ${p1.now()}`)
let p2 = new p1();
console.log(`p2.getFullYear(): ${p2.getFullYear()}`);

The output is:

p1 descriptors: length,name,prototype,now,parse,UTC
p1.now(): 1628887199300
p2.getFullYear(): 2021

So the proxy wrapper is successfully forwarding the calls, it just isn't intercepting them.

Mark J Miller
  • 4,751
  • 5
  • 44
  • 74
  • The `apply` trap is only relevant when *executing* the object. So, you can use it if you have a proxy over a function. It is *not* used when calling a method on an object. – VLAZ Aug 13 '21 at 20:45
  • 2
    That output doesn’t match the code (`p2` vs. `p3`), by the way. You can use Stack Snippets to let people run the code for themselves (icon looks like `<>` in the toolbar). – Heretic Monkey Aug 13 '21 at 20:50

2 Answers2

1

The apply trap only handles direct calls to a function. If you want to intercept invocations of the new operator, you need to use the construct trap:

let p1 = new Proxy(Date, {
  apply: function(target, that, args) {
      console.log(`function called ${target.name}`);
      return target.apply(args);
  },
  construct: function(target, that, args) {
    console.log(`function constructed ${target.name}`);
    return Reflect.construct(...arguments)
  }
});
console.log(`p1 descriptors: ${Object.getOwnPropertyNames(p1)}`);

console.log(`p1.now(): ${p1.now()}`)
let p2 = new p1();
console.log(`p2.getFullYear(): ${p2.getFullYear()}`)
VLAZ
  • 26,331
  • 9
  • 49
  • 67
  • Does that mean I need to wrap the result of `construct` in a proxy as well? I tried it and it throws an error `Uncaught TypeError: this is not a Date object` – Mark J Miller Aug 13 '21 at 20:57
  • Or does it mean I have to wrap all the functions of the result with an `apply` trap? – Mark J Miller Aug 13 '21 at 20:59
  • You *could* wrap it in a proxy, if you want. But it depends on what you intend to do. The `construct` trap just allows you to use custom logic for what happens with `new` calls. If you want to return proxied objects, then you can invoke the normal construction (`Reflect.construct` is useful here) and then wrap that in a proxy. The `apply` trap is *not* triggered when you call the proxy with `new`. – VLAZ Aug 13 '21 at 21:00
  • I'm trying to intercept all the functions of `Date`. Both the object functions (e.g., `now`, `parse`, `UTC`) as well as the instance methods from the prototype. – Mark J Miller Aug 13 '21 at 21:05
  • You then need a `get` trap and wrap all the results in a proxy with an `apply` trap – VLAZ Aug 13 '21 at 21:06
  • 2
    …and you'd still have problems since `Date` is a builtin, [just like `Map`](https://stackoverflow.com/q/43236329/1048572). See also [this](https://stackoverflow.com/a/47874647/1048572). – Bergi Aug 13 '21 at 22:10
  • 2
    @VLAZ You don't have to wrap the method `get` results in proxies, you can also simply wrap them in closures. – Bergi Aug 13 '21 at 22:11
0

I was able to find the answer thanks to @VLAZ, but I wanted to post the completed answer here as the original question was trying to get the apply method to work. I also followed the example in @Bergi's comment which used bind to fix the problem w/ date and allowed me to reuse the function definition for the get trap:

let get = function(getTarget, p) {
  console.log(`get called: ${getTarget.name || p}`);
  return new Proxy(getTarget[p], {
    apply: function(applyTarget, that, args) {
      console.log(`function called: ${applyTarget.name}`);
      return Reflect.apply(applyTarget, getTarget, args);
    }
  });
}


let handler = {
  construct: function(t1, argArray) {
    console.log(`ctor called ${t1.name}`);
    let result = Reflect.construct(t1, argArray);
    let p = new Proxy(result, {
      get: get,
    });
    return p;
  },
  get: get
};

let p1 = new Proxy(Date, handler);
console.log(`p1 descriptors: ${Object.getOwnPropertyNames(p1)}`);
console.log(`p1.now(): ${p1.now()}`)

let p2 = new p1();
console.log(`p2 descriptors: ${Object.getOwnPropertyNames(Object.getPrototypeOf(p2))}`);
console.log(`p2.getFullYear(): ${p2.getFullYear()}`);
Mark J Miller
  • 4,751
  • 5
  • 44
  • 74
  • 1
    `target.apply(...args)` is wrong, that eats up the arguments. (Only date methods normally don't take any argument so it still works). You should use `target.apply(null, args)` or `target.call(null, ...args)` or just `Reflect.apply(target, null, args)`. If you do `Reflect.apply(methodTarget, dateTarget, args)` you don't even need to `.bind()` the method. – Bergi Aug 13 '21 at 23:11
  • 1
    I wasn't suggesting to use a proxy over the bound function. Rather, just do either `return target[p].bind(target)` or `return (...args) => { console.log(…); return target[p](...args); }`. No proxy necessary here, unless you want to further intercept property accesses like `Date.now.length` or `dateInstance.getFullYear.call` – Bergi Aug 13 '21 at 23:13
  • Thanks, @Bergi. I've fixed the swallowing of `args` and replaced `bind` with `Reflect.apply` as you mentioned. I did originally implement a closure instead of nesting apply as you recommend, but I actually would like to potentially capture the extra level. I'm using this to track active browser fingerprinting and I'd like to know exactly what values are being used to fingerprint the current browser. – Mark J Miller Aug 16 '21 at 20:39