Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is Proxy to a Map object in ES2015 not working

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.)

like image 461
Rand Avatar asked Apr 05 '17 16:04

Rand


1 Answers

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"));
like image 73
T.J. Crowder Avatar answered Oct 30 '22 13:10

T.J. Crowder