Join GitHub today
GitHub is home to over 28 million developers working together to host and review code, manage projects, and build software together.
Sign upObjects created from proxy of bound function don't get the expected prototype #1052
Comments
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
bergus
Dec 20, 2017
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).
bergus
commented
Dec 20, 2017
Unfortunately, this expectation doesn't hold for most native |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
ljharb
Dec 20, 2017
Member
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?
|
Functions in particular, because of |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
claudepache
Dec 20, 2017
Contributor
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.
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. |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
ljharb
Dec 20, 2017
Member
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).
|
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). |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
claudepache
Dec 20, 2017
Contributor
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.
|
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, 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 ( |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
allenwb
Dec 21, 2017
Member
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.
|
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
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. |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
ljharb
Dec 21, 2017
Member
@allenwb given that, is there any reason not to have something equivalent to Proxy.isProxy?
|
@allenwb given that, is there any reason not to have something equivalent to |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
claudepache
Dec 21, 2017
Contributor
@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 */ }).
|
@ljharb The issue of non-transparency just means that implementing correctly an object with the help of |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
LucaFranceschini
Dec 21, 2017
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...
</noob rant>
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.
LucaFranceschini
commented
Dec 21, 2017
•
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... </noob rant> 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 |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
bergus
Dec 21, 2017
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.
bergus
commented
Dec 21, 2017
I think it's fundamentally impossible to make a proxy completely transparent for a target with internal slots. In 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. |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
ljharb
Dec 21, 2017
Member
@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).
|
@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). |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
bakkot
Dec 21, 2017
Contributor
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.
|
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. |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
Perelandric
Dec 21, 2017
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); // falseSo a proxied bound function is related to the original's closure, but unrelated to its .prototype object.
Perelandric
commented
Dec 21, 2017
•
|
A Proxy instance is not a function, but it makes us believe it is via 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); // falseSo a proxied bound function is related to the original's closure, but unrelated to its |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
ljharb
Dec 21, 2017
Member
@Perelandric a proxy instance of a function has a [[Call]] internal method, which is what typeof checks, and what makes it a function.
|
@Perelandric a proxy instance of a function has a [[Call]] internal method, which is what typeof checks, and what makes it a function. |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
erights
Dec 21, 2017
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
commented
Dec 21, 2017
•
|
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 F.prototype.toString.call(p) everything works fine. The only reason that the present issue is a genuine problem is that |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
ljharb
Dec 21, 2017
Member
@erights would it be worth making Function.prototype.toString unwrap proxies, which would make proxies to functions indistinguishable (like arrays)?
|
@erights would it be worth making |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
Perelandric
Dec 21, 2017
@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.
Perelandric
commented
Dec 21, 2017
|
@ljharb That is the mechanics of 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. |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
erights
Dec 21, 2017
@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.
erights
commented
Dec 21, 2017
•
|
@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. |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
allenwb
Dec 21, 2017
Member
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.
|
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. |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
erights
Dec 21, 2017
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
erights
commented
Dec 21, 2017
•
|
I favor the full pass-through. There is no realistic scenario where Attn: @tvcutsem @ajvincent |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
|
@bakkot are you calling everything with internal slots exotic? |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
erights
Dec 23, 2017
@littledan yes, everything with internal slots, beyond the universal ones, is exotic.
erights
commented
Dec 23, 2017
|
@littledan yes, everything with internal slots, beyond the universal ones, is exotic. |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
bakkot
Dec 23, 2017
Contributor
@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).
|
@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). |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
evilpie
Dec 23, 2017
Contributor
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
|
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 |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
LucaFranceschini
referenced this issue
Jan 12, 2018
Open
`instanceof` throws on wrapped bound functions #25
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
LucaFranceschini
Jan 31, 2018
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.
LucaFranceschini
commented
Jan 31, 2018
•
|
FWIW, a possible work-around for the unexpected behavior I described in the OP is to manually fix 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.
|
LucaFranceschini commentedDec 20, 2017
•
edited
This prints
undefinedwhile I expectedbaz. (Much) More details here. Long story short: theprototypeproperty of the proxy object is used (which forwards to theprototypeproperty of the bound function), which looks... weird.More generally, I would expect
new Proxy(O, { })to behave exactly likeO.Bound functions actually do a trick, they set
new.targetto 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?