15

I'm running the following script through Google Chrome Version 57.0.2987.133:

var loggingProxyHandler = {
    "get" : function(targetObj, propName, receiverProxy) {
        let ret = Reflect.get(targetObj, propName, receiverProxy);
        console.log("get("+propName.toString()+"="+ret+")");
        return ret;
     },

     "set" : function(targetObj, propName, propValue, receiverProxy) {
         console.log("set("+propName.toString()+"="+propValue+")");
         return Reflect.set(targetObj, propName, propValue, receiverProxy);
     }
};

function onRunTest()
{
    let m1 = new Map();
    let p1 = new Proxy(m1, loggingProxyHandler);
    p1.set("a", "aval");   // Exception thrown from here
}

onRunTest();
NOTE: Requires a browser supporting ES2015's Proxy

When run, I see the handler's get trap is called to return the Map's set function and then I receive the following error:

"Uncaught TypeError: Method Map.prototype.set called on incompatible receiver [object Object]"
at Proxy.set (native)
...

I tried removing the trap functions from the loggingProxyHandler (making it an empty object) but still receive the same error.

My understanding was that a Proxy object was supposed to be able to generated for all native ES5 and ES2015 javascript objects. Array seems to work well under the same proxy handler. Did I misunderstand the specs?
Is my code missing something? Is there a known bug in Chrome? (I did a search and found no defects for Chrome on this subject.)

user3840170
  • 26,597
  • 4
  • 30
  • 62
Rand
  • 151
  • 1
  • 3
  • possible duplicate of [Why is my Proxy wrapping a Map's function calls throwing TypeError?](http://stackoverflow.com/questions/42381028/why-is-my-proxy-wrapping-a-maps-function-calls-throwing-typeerror) – Bergi Apr 05 '17 at 16:13
  • 1
    It sounds like what you actually wanted to do is override (intercept) `set` and `get` calls, not route all *property accesses* through a proxy? – Bergi Apr 05 '17 at 16:16
  • To make it clear: Don't use `Proxy` to intercept exotic behavior, which goes beyond normal `Object` semantics. Use subclassing instead. –  Oct 07 '17 at 10:57
  • 2
    To make it clearer: there are perfectly valid cases when you might need to end up proxying a map, especially when you don't own the original object graph but wish to observe any changes to it. – theMayer Feb 20 '20 at 03:54

2 Answers2

33

The reason you're getting the error is that the proxy isn't getting involved in the p1.set() method call (other than that the get trap is used to retrieve the function reference). So once the function reference has been retrieved, it's called with this set to the proxy p1, not the map m1 — which the set method of a Map doesn't like.

If you're really trying to intercept all property access calls on the Map, you can fix it by binding any function references you're returning from get (see the *** lines):

const loggingProxyHandler = {
    get(target, name/*, receiver*/) {
        let ret = Reflect.get(target, name);
        console.log(`get(${name}=${ret})`);
        if (typeof ret === "function") {    // ***
          ret = ret.bind(target);           // ***
        }                                   // ***
        return ret;
     },

     set(target, name, value/*, receiver*/) {
         console.log(`set(${name}=${value})`);
         return Reflect.set(target, name, value);
     }
};

function onRunTest() {
    const m1 = new Map();
    const p1 = new Proxy(m1, loggingProxyHandler);
    p1.set("a", "aval");
    console.log(p1.get("a")); // "aval"
    console.log(p1.size);     // 1
}

onRunTest();
NOTE: Requires a browser supporting ES2015's Proxy

Notice that when calling Reflect.get and Reflect.set, we don't pass along the receiver (in fact, we're not using the receiver argument at all in those, so I've commented the parameter out). That means they'll use the target itself as the receiver, which you need if the properties are accessors (like Map's size property) and they need their this to be the actual instance (as Map's size does).


If your goal is just to intercept Map#get and Map#set, though, you don't need a proxy at all. Either:

  1. Create a Map subclass and instantiate that. Assumes you control the creation of the Map instance, though.

  2. Create a new object that inherits from the Map instance, and override get and set; you don't have to be in control of the original Map's creation.

  3. Replace the set and get methods on the Map instance with your own versions.

Here's #1:

class MyMap extends Map {
  set(...args) {
    console.log("set called");
    return super.set(...args);
  }
  get(...args) {
    console.log("get called");
    return super.get(...args);
  }
}

const m1 = new MyMap();
m1.set("a", "aval");
console.log(m1.get("a"));

#2:

const m1 = new Map();
const p1 = Object.create(m1, {
  set: {
    value: function(...args) {
      console.log("set called");
      return m1.set(...args);
    }
  },
  get: {
    value: function(...args) {
      console.log("get called");
      return m1.get(...args);
    }
  }
});

p1.set("a", "aval");
console.log(p1.get("a"));

#3:

const m1 = new Map();
const m1set = m1.set; // Yes, we know these are `Map.prototype.set` and
const m1get = m1.get; // `get`, but in the generic case, we don't necessarily
m1.set = function(...args) {
  console.log("set called");
  return m1set.apply(m1, args);
};
m1.get = function(...args) {
  console.log("get called");
  return m1get.apply(m1, args);
}

m1.set("a", "aval");
console.log(m1.get("a"));
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • It looks like I'll need to go with a subclass mechanism if I want to achieve some level of AOP here. But then access to the Map's size property is problematic because it's not achieved through a method call. When I added the recommended bind, it indeed got around the exception, however once the function is bound to the targetObj through the get trap, the set trap will not be called, effectively nullifying the Proxy. I suppose if I want to pursue the Proxy I could try to generate a completely new function, bound to the targetObject but calling the proxy handler trap (through a closure). – Rand Apr 05 '17 at 18:41
  • @Rand: Yup. There's no reason the `set` trap would be called, nothing is *setting* a property. I don't see why accessing `size` is problematic: Properties are inherited, just access it. – T.J. Crowder Apr 05 '17 at 20:00
  • @T.J.Crowder You say "I don't see why accessing size is problematic: Properties are inherited, just access it." I don't see why either, but it is. On Chrome 59, your first example works but if I add `console.log(p1.size)` it gives `Uncaught TypeError: Method Map.prototype.size called on incompatible receiver [object Object]`. Any ideas? Your example is the closest I've seen a Map proxy to working. – Don Hatch May 05 '17 at 12:31
  • Huh, apparently I can make the proxy fully functional by inserting `if (name === 'size') { return targetObj.size; }` before the call to `Reflect.get()` in your first example. No idea why, but this is great! I think it will solve another problem I'm working on: http://stackoverflow.com/questions/43801605/ – Don Hatch May 05 '17 at 13:55
  • @DonHatch That seems to indicate that the `.size` property is getter, which acts like a function with respect to `this`. If I'm not mistaken, another way to do it would be to grab the getter from the descriptor call it: `Object.getOwnPropertyDescriptor(target, 'size').get.call(target)` and that should do the trick, which is effectively the same as `target.size`. :) – trusktr Nov 19 '19 at 08:04
  • @DonHatch - Sorry, I didn't see those comments (just saw them now thanks to trusktr's reply). I've updated the proxy example above and added a bit of text after it to explain about the `size` property and ones like it. Happy coding! – T.J. Crowder Nov 19 '19 at 09:39
  • This is brilliant. Thank you. – theMayer Feb 20 '20 at 03:50
10

Let me add more to this.

Many built-in objects, for example Map, Set, Date, Promise and others make use of so-called internal slots.

These are like properties but reserved for internal, specification-only purposes. For instance, Map stores items in the internal slot [[MapData]]. Built-in methods access them directly, not via [[Get]]/[[Set]] internal methods. So Proxy can’t intercept that.

For example:

let map = new Map();

let proxy = new Proxy(map, {});

proxy.set('name', 'Pravin'); // Error

Internally, a Map stores all data in its [[MapData]] internal slot. The proxy doesn't have such slot. The built-in method Map.prototype.set method tries to access the internal property this.[[MapData]], but because this=proxy, can't find it in proxy and just fails.

There’s a way to fix it:

let map = new Map();
let proxy = new Proxy(map,{
    get(target,prop,receiver){
        let value = Reflect.get(...arguments);
        return typeof value === 'function'?value.bind(target):value;
    }
});
proxy.set('name','Pravin');
console.log(proxy.get('name')); //Pravin (works!)

Now it works fine, because get trap binds function properties, such as map.set, to the target object (map) itself. So the value of this inside proxy.set(...) will be not proxy, but the original map. So when the internal implementation of set tries to access this.[[MapData]] internal slot, it succeeds.

Pravin Divraniya
  • 4,223
  • 2
  • 32
  • 49
  • Thanks for your answer. How do you tell if a variable has one of these internal slots? – Andrew Jun 15 '20 at 06:05
  • 1
    @Andrew Each object in an ECMAScript engine is associated with a set of internal methods that defines its runtime behaviour. These internal methods are not part of the ECMAScript language. They are defined by this specification purely for expository purposes. However, each object within an implementation of ECMAScript must behave as specified by the internal methods associated with it. The exact manner in which this is accomplished is determined by the implementation. – Pravin Divraniya Jun 15 '20 at 08:21
  • 1
    Internal slots correspond to internal state that is associated with objects and used by various ECMAScript specification algorithms. Internal slots are not object properties and they are not inherited. Depending upon the specific internal slot specification, such state may consist of values of any ECMAScript language type or of specific ECMAScript specification type values. – Pravin Divraniya Jun 15 '20 at 08:24
  • 2
    “Internal Slots” are an implementation detail. They are not object properties. JavaScript programmers don't need to know about internal slots but are very useful to explain some JavaScript behaviour. Hope this will help. – Pravin Divraniya Jun 15 '20 at 08:24