3

I use a Map javascript object. I'd like to observe changes to a Map instance using a Proxy object. However, whatever handler object I try to feed in to the proxy, I consistently get the error Method Map.prototype.set called on incompatible receiver. What am I doing wrong? The idiomatic example I expect to work looks like

var map = new Map();
var proxy = new Proxy(map, {
    set(obj, prop, value) {
        console.log(value);
    }
});
proxy.set(1, 1); --> error

I also tried setting a handler for apply, but to no avail.

Ruudjah
  • 807
  • 7
  • 27
  • Nothing you're doing in your snippet will actually trigger a `set` trap in the first place. Your code also reproduces if you do `new Proxy(new Map, {}).set(1, 1)` – loganfsmyth Jan 25 '18 at 23:29
  • You are neither assigning properties to the proxy nor are you calling it as a function, that's why neither the `set` nor `apply` traps work – Bergi Jan 25 '18 at 23:52
  • @Bergi Ugh I searched and could not find a good duplicate, alas. – loganfsmyth Jan 25 '18 at 23:54

1 Answers1

4

First off, understand that your error also reproduces with just

var map = new Map();
var proxy = new Proxy(map, {});
proxy.set(1, 1);

It is not related to your usage of set(obj, prop, value).

Why it fails

To break that a bit more, understand that this is basically the same as doing

var map = new Map();
var proxy = new Proxy(map, {});
Map.prototype.set.call(proxy, 1, 1);

which also errors. You are calling the set function for Map instances, but passing it a Proxy instead of a Map instance.

And that is the core of the issue here. Maps store their data using an private internal slot that is specifically associated with the map object itself. Proxies do not behave 100% transparently. They allow you to intercept a certain set of operations on an object, and perform logic when they happen, which usually means proxying that logic through to some other object, in your case from proxy to map.

Unfortunately for your case, proxying access to the Map instance's private internal slot is not one of the behaviors that can be intercepted. You could kind of imagine it like

var PRIVATE = new WeakMap();
var obj = {};

PRIVATE.set(obj, "private stuff");

var proxy = new Proxy(obj, {});

PRIVATE.get(proxy) === undefined // true
PRIVATE.get(obj) === "private stuff" // true

so because the object pass as this to Map.prototype.set is not a real Map, it can't find the data it needs and will throw an exception.

Solution

The solution here means you actually need to make the correct this get passed to Map.prototype.set.

In the case of a proxy, the easiest approach would be to actually intercept the access to .set, e.g

var map = new Map();

var proxy = new Proxy(map, {
  get(target, prop, receiver) {
    // Perform the normal non-proxied behavior.
    var value = Reflect.get(target, prop, receiver);

    // If something is accessing the property `proxy.set`, override it
    // to automatically do `proxy.set.bind(map)` so that when the
    // function is called `this` will be `map` instead of `proxy`.
    if (prop === "set" && typeof value === "function") value = value.bind(target);

    return value;
  }
});

proxy.set(1, 1); 

Of course that doesn't address your question about intercepting the actual calls to .set, so for that you can expand on this to do

var map = new Map();

var proxy = new Proxy(map, {
  get(target, prop, receiver) {
    var value = Reflect.get(target, prop, receiver);
    if (prop === "set" && typeof value === "function") {
      // When `proxy.set` is accessed, return your own
      // fake implementation that logs the arguments, then
      // calls the original .set() behavior.
      const origSet = value;
      value = function(key, value) {
        console.log(key, value);

        return origSet.apply(map, arguments);
      };
    }
    return value;
  }
});

proxy.set(1, 1);
loganfsmyth
  • 156,129
  • 30
  • 331
  • 251