Why is Proxy to a Map object in ES2015 not working
The reason you're getting the error is that the proxy isn't getting involved in the p1.set
call (other than that the set
trap — unrelated, despite same name — is getting called to retrieve the function reference). So once the function reference has been retrieved, it's called with this
set to the proxy, not the Map
— which 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:
Create a
Map
subclass and instantiate that. Assumes you control the creation of theMap
instance, though.Create a new object that inherits from the
Map
instance, and overrideget
andset
; you don't have to be in control of the originalMap
's creation.Replace the
set
andget
methods on theMap
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` andconst m1get = m1.get; // `get`, but in the generic case, we don't necessarilym1.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"));
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.