Why Is Proxy to a Map Object in Es2015 Not Working

Why is Set not executed when using Proxy on Map Object?

If you want a proxy setup that lets you intercept attempted calls to the Map .set() method, you'd have to do something like this:

let handler = {
get: (target, property, proxy) {
if (property === "set") {
console.log("intercepted a '.set' access on the proxied map");
return (key, value) => {
console.log("intercepted a 'set()' call on the proxied map");
return target.set(key, value);
};
}
return target[property];
}
};

The "get" handler method is called for any property access is attempted. In the method call

proxy.set("some key", "some value");

the "set" property of the object has to be looked up before the method can actually be called. It's that lookup operation that results in the handler "get" method invocation.

Now if you make the proxy as in your code and then do

proxy.set("some key", "some value");

you'll see that the log message fires when that "interceptor" function you returned is invoked.

Observe changes to a Map using a Proxy

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

Why is my Proxy wrapping a Map's function calls throwing TypeError?

apply traps calling (if you were proxying a function), not method calls (which are just property access, a call, and some this shenanigans). You could supply get and return a function instead:

var cache = new Proxy(new Map(), {
get(target, property, receiver) {
return function () {
console.log('hello world!');
};
}
});

I don’t suppose you’re just trying to override parts of Map, though? You can inherit from its prototype in that case (this is a much better option than the proxy if it’s an option at all):

class Cache extends Map {
set(key, value) {
console.log('hello world!');
}
}

const cache = new Cache();

new Proxy(new Map(), {}).values

So it seems that Map methods complain if its thisArg is not an actual Map object. One solution would be to add a get to the proxy that checks if the property being fetched is a function, and if so, it returns a function that calls the requested function using the original, non-Proxy object.

const things = new Proxy(new Map(), {
set(t, k, v) {
console.log(t, k, v);
Reflect.set(t, k, v);
},
get(t, k) {
if (typeof t[k] === "function") {
return (...args) => Reflect.apply(t[k], t, args)
}
return t[k];
}
});

One potential down-side to this is that the function returned will be effectively bound to the original map. Probably not an issue for most cases, but it will render calls like things.values.call(someOtherMap) useless. There are probably ways to work around this if it's an issue.

Why is Set Incompatible with Proxy?

I am attempting to Proxy() a Set()

However, you haven't used any of the available traps - there is no add one. All that you can intercept in the call p.add(55) is the property access to .add on the proxy, which goes through the get trap and returns a function.

If you want to intercept calls to the add method, you don't need a proxy at all, better (subclass and) overwrite that method similar to how .set was overridden here and here for Map.

proxying a Set() in any way breaks it categorically

Yes, because the proxy is not a Set any more.

var s = new Set([42]);
var p = new Proxy(s, {});
s.has(42) // true
console.log(s === p) // false
p.has.call(s, 42) // true
p.has(42) // exception - calls `has` on `p`, not on `s`

Calling Set methods on objects that are no True Sets does throw an exception (which e.g. can be used for detecting them). For your particular case, see ECMAScript 6 §23.2.3.1:

"If S does not have a [[SetData]] internal slot, throw a TypeError exception."

And indeed, p is a proxy (which does have the internal Proxy methods and slots, especially [[ProxyHandler]] and [[ProxyTarget]]) instead of a set like s with its [[SetData]] internal slot.

You reasonably were expecting that "if a trap has not been defined, the default behavior is to forward the operation to the target", however that only applies to standard behaviours like property access, and not the internal slots of exotic objects.

Why isn't ownKeys Proxy trap working with Object.keys()?

The reason is simple: Object.keys returns only properties with the enumerable flag. To check for it, it calls the internal method [[GetOwnProperty]] for every property to get its descriptor. And here, as there’s no property, its descriptor is empty, no enumerable flag, so it’s skipped.

For Object.keys to return a property, we need it to either exist in the object, with the enumerable flag, or we can intercept calls to [[GetOwnProperty]] (the trap getOwnPropertyDescriptor does it), and return a descriptor with enumerable: true.

Here’s an example of that:

let user = { };

user = new Proxy(user, {
ownKeys(target) { // called once to get a list of properties
return ['a', 'b', 'c'];
},

getOwnPropertyDescriptor(target, prop) { // called for every property
return {
enumerable: true,
configurable: true
/* ...other flags, probable "value:..." */
};
}

});

console.log( Object.keys(user) ); // ['a', 'b', 'c']


Related Topics



Leave a reply



Submit