function Foo() { }
Foo.prototype.bar = 'baz'
const Bound = Foo.bind(42) // just to build a bound function
, Proxied = new Proxy(Bound, { }) // empty handler
console.log(new Proxied().bar)
This prints undefined while I expected baz. (Much) More details here. Long story short: the prototype property of the proxy object is used (which forwards to the prototype property of the bound function), which looks... weird.
More generally, I would expect new Proxy(O, { }) to behave exactly like O.
Bound functions actually do a trick, they set new.target to the original function so that everything works as expected (step 5 of this). I would expect Proxy objects to do the same, maybe here.
What is the rationale behind this choice?
More generally, I would expect
new Proxy(O, {})to behave exactly likeO.
Unfortunately, this expectation doesn't hold for most native O objects with internal slots, like sets, iterators, dates or maps. Bound functions are just another case. (That said, I tend to agree that something should be done on this problem).
Functions in particular, because of Function.prototype.toString, are distinguishable from Proxies. Arrays, however, are not, because of the internal isArray Proxy unwrapping. Should perhaps functions have the same unwrapping, bound or otherwise?
More generally, I would expect new Proxy(O, {}) to behave exactly like O.
Unfortunately, this expectation doesn't hold for most native O objects with internal slots
Another case where things could break, is when an algorithm relies on some identity, because identity cannot be proxied. Because of this, even proxying a random plain object may lead to surprises.
For bound constructors, the offending step mentioned in Comment 0 (step 5 of this) precisely relies on identity.
However, just like the isArray abstract operation, it could unwrap the Proxy (as long as it didn’t set the unwrapped value to be the target).
Another thing broken with proxied bound functions is the instanceof operator, because the algorithm branches on the presence of the [[BoundTargetFunction]] internal slot. As a result, qux instanceof Proxied will throw instead of returning a boolean.
Having scanned the uses in the spec of the [[Bound*]] internal slots as well as the expression “bound function”, I think that only those two cases (new and instanceof) are problematic.
isArray drilling throw proxies when the target object is an Array exotic object was a late addition to ES6 and one that I opposed.
The argument in favor was that people would just expect it to work that way. The reason I opposed it was exactly the reasons being discussed in this thread. ES proxies, in fact, are not transparent forwarders and there are many ways to trip over this lack of transparency. It was easy enough to get Array.isArray to drill through as a special case. But, as we see here, that just creates the expectation that other non-proxy transparent characteristics of various objects should also be special cased to create the illusion of transparency. But there are too many of them. (for example class private fields will have the similar proxying issues as internal slots).
People who expect
new Proxy(O, {}) to behave exactly like O.
don't understand ES proxies. If they are going to use then, they need to correct that deficiency. Adding special cases that make some Proxies appear to be transparent simply adds to the confusion.
@allenwb given that, is there any reason not to have something equivalent to Proxy.isProxy?
@ljharb Proxy.isProxy (like Function.isGenerator) gives information about how the object is implemented, not how it behaves. It is often the answer to the wrong question.
The issue of non-transparency just means that implementing correctly an object with the help of Proxy requires more work than new Proxy(O, { /* empty */ }).
isArray drilling throw proxies when the target object is an Array exotic object was a late addition to ES6 and one that I opposed.
The argument in favor was that people would just expect it to work that way. The reason I opposed it was exactly the reasons being discussed in this thread. ES proxies, in fact, are not transparent forwarders and there are many ways to trip over this lack of transparency. It was easy enough to get Array.isArray to drill through as a special case. But, as we see here, that just creates the expectation that other non-proxy transparent characteristics of various objects should also be special cased to create the illusion of transparency. But there are too many of them. (for example class private fields will have the similar proxying issues as internal slots).
People who expect
new Proxy(O, {}) to behave exactly like O.don't understand ES proxies. If they are going to use then, they need to correct that deficiency. Adding special cases that make some Proxies appear to be transparent simply adds to the confusion.
Ok, ES proxies just don't work like that, point taken. However, generally speaking, proxies are expected to be as much behaviorally equivalent as possible to the wrapped object (expect for identity, of course). That's how the proxy pattern is known, so it must not come as a surprise if ES proxies generate confusion. What I'm saying is that I can take an apple, call it a banana and tell everyone expecting it to be a banana they're wrong. Of course I would be technically right, but still, naming matters...
Coming back to comment 0: would it be sensible to set _newTarget_ to the target function object if it was the proxy itself, before going on with construction, just like bound functions do? Furthermore, in the example I made, not only the result may be unexpected but also misleading, since _you can_ access a prototype property through the proxy, but doing so will give the bound function property and not the original function's one.
@claudepache
The issue of non-transparency just means that implementing correctly an object with the help of Proxy requires more work than
new Proxy(O, { /* empty */ }).
I think it's fundamentally impossible to make a proxy completely transparent for a target with internal slots. In p = new Proxy(t, { /* elaborate sophisticated something */ }), you either get p.method() to work or get p.method === t.method (assuming you do not want to harm t or its prototype).
To solve this problem, we would need every single slot lookup to (recursively?) unwrap proxies. I agree with @allenwb that this is not reasonable.
In general, when wrapping instances of builtin types in a proxy, one actually needs to intercept all the method calls that mutate the internal slots of the object. Subclassing is by far better suited for that purpose anyway, and if necessary one can still inject the proxy in the prototype chain.
@claudepache right; i'm not saying it's a useful thing to know, i'm asking if there's any technical reason why we couldn't expose that functionality. The existence of an "is proxy" function would certainly, imo, mitigate confusion around proxy transparency (namely, that proxies can only be transparent when all of the builtins can also be proxied).
cc @erights
I believe an ordinary object can be transparently proxied currently, at least if its prototype chain is locked down. It's only functions and exotics which have this issue.
A Proxy instance is not a function, but it makes us believe it is via typeof. So what function is it? I think the most reasonable answer is that it is the function it wraps, even if not by identity.
Also consider closures. It seems inconsistent that a proxied bound function uses the same closure of the original function but doesn't use its prototype.
const F = (function(n) {
return function() { this.foo = ++n };
})(0);
const BF = F.bind({});
const PBF = new Proxy(BF, {});
console.log(new F().foo); // 1
console.log(new BF().foo); // 2
console.log(new PBF().foo); // 3
console.log(new F() instanceof F); // true
console.log(new BF() instanceof F); // true
console.log(new PBF() instanceof F); // false
So a proxied bound function is related to the original's closure, but unrelated to its .prototype object.
@Perelandric a proxy instance of a function has a [[Call]] internal method, which is what typeof checks, and what makes it a function.
I do not have time to read this thread right now, so apologies if this has already been covered.
The goal of proxies has never been that an individual proxy be transparent with high fidelity. It is that membranes be transparent with high fidelity. Class private state do not threaten that in the slightest. Internal properties, under normal use patterns, do not threaten that either. The relevant difference between private state and internal properties is only that the former is per realm whereas the latter is cross-realm. If there were no cross-realm visibility of internal properties to builtin operations, then they would be as unproblematic as class private state.
If I have a dry function proxy p to a wet function g, and a dry function proxy F to a wet Function and I do
F.prototype.toString.call(p)
everything works fine. The only reason that the present issue is a genuine problem is that Function.prototype.toString works on functions from other realms.
@erights would it be worth making Function.prototype.toString unwrap proxies, which would make proxies to functions indistinguishable (like arrays)?
@ljharb That is the mechanics of typeof, and is what makes typeof return the string "function". But that's just another way of saying that the decision was made to lead us to believe that it is a function. Certainly it only has [[Call]] by virtue of the proxied function, which brings us back to the original question.
I think it would be hard to deny the inconsistency of there sometimes being an implied relationship to a specific function object, and sometimes not.
@ljharb Perhaps. First, I apologize for having missed this issue --- I should have seen this one coming well before it became an issue in practice.
The reason why it would be ok in this case is that the internal slot in question holds only the source code string which would be rendered, and so communicates only data that is available to anyone with a direct reference to the underlying function anyway.
The reason it might still not be ok is that the source code of the underlying function does not necessarily represent the [[Call]] behavior of the proxy, even though the proxy's [[Call]] behavior includes calling that function's [[Call]]. This counter-argument is worth discussing.
The reason it might be ok is that the [[Call]] behavior of the target represents the likely behavior of the proxy, when the proxy is part of a membrane trying to be transparent. This stance would demote the security role of Function.prototype.toString to presenting the source code the function alleges represents its [[Call]] behavior.
If we decide against, Function.prototype.toString.call(functionProxy) should return a string that conforms to the spec for how builtin functions print, i.e.,
function foo() { [native code] }
A function proxy is a callable and should definitely do one or the other. Of these two options, I don't yet know which I favor.
Note that one of the reason that Array.isArray is problematic is that is pierces the encapsulation barrier of the internal implementation of objects. Specifically it knows how, at the implementation level, to identify an "array exotic object" and a "proxy exotic object".
One of the design use cases for Proxy was to enable self-hosting of built-ins that are exotic. An implementation that actually used Proxy to self-host built-in Array instances would have to have some sort of implementation dependent way to identify Array instances and its implementation of Array.isArray would have to use that identification mechanism in its implementation of step 2 of IsArray.
I favor the full pass-through. There is no realistic scenario where Function.prototype.toString.call provides any stronger guarantee than representing the [[Call]] behavior that the function alleges itself to have. Since there is no forced veracity guarantee anyway, we may as well go all the way.
Attn: @tvcutsem @ajvincent
@bakkot are you calling everything with internal slots exotic?
@littledan yes, everything with internal slots, beyond the universal ones, is exotic.
@allenwb @bterlson do I have that right?
@littledan Sorry, wrong term; I always forget that "exotic object" means something specific in the spec, not just "weird object". I mean roughly "exotic objects + objects with extra slots", I guess.
@erights: since I just looked it up myself: the spec defines "exotic object" to be an object which has non-default behavior for at least one of the standard internal methods (basically the things a proxy can intercept). This is not quite equivalent to "has an extra internal slot"; for example Maps have their own internal slots but are not "exotic objects" (I think).
In Firefox we used to support native functions called on proxied objects. We still have support for this to make cross-compartment calls and other wrappers work. https://searchfox.org/mozilla-central/rev/78bc55ae1f1909be5ffc66c0ec447accc639edd3/js/public/CallNonGenericMethod.h#31
Oh, I should not that this is quite tricky to do correctly. We aren't completely ready to handle this. See bug 1111243
@erights I agree with @bakkot fwiw, an ordinary object can have any number of slots as long as it has the default behavior for the essential internal methods.
FWIW, a possible work-around for the unexpected behavior I described in the OP is to manually fix new.target to the original function, if it appears to be the proxy. But there may be corner cases I'm not taking into account.
function Foo () { }
Foo.prototype.bar = 'baz'
const Bound = Foo.bind(42)
new Bound().bar // 'baz'
const handler = { }
const Proxied = new Proxy(Bound, handler)
handler.construct = (target, args, newTarget) =>
Reflect.construct(target, args, newTarget === Proxied ? target : newTarget)
new Proxied().bar // 'baz'
Caveat: the handler need a reference to the proxy, which in turns needs an existing handler to be created.
toString on proxies is still a problem though.
Hopefully summarizing the thread:
Given all that, I'm going to close this, but will be happy to reopen if it's desired.
@ljharb Thanks for summarizing before closing. Maybe it's worth mentioning this as well:
https://github.com/tc39/ecma262/issues/1052#issuecomment-353195149
Most helpful comment
I favor the full pass-through. There is no realistic scenario where
Function.prototype.toString.callprovides any stronger guarantee than representing the [[Call]] behavior that the function alleges itself to have. Since there is no forced veracity guarantee anyway, we may as well go all the way.Attn: @tvcutsem @ajvincent