Open dead-claudia opened 6 years ago
@isiahmeadows this is pretty much the same as in #4, which I didn't detailed very well but certainly it needs the new internal slot. There are few differences that I will like to clarify:
O.[[ResolveReceiver]](key, receiver)
needs a key? IMO, this operation should be about what object should the engine use for a certain operation, not about what operation is the engine about to performance on that object.Reflect.resolve
?@caridy Mine is similar to that proposal, but not quite the same. Here's the two main concrete differences:
The first opens up a few other use cases beyond simple unwrapping, like multiple delegation or other computed, property-specific receivers. The second is just simplifying the unwrapping, preferring to do less for something called on basically every property access.
- why O.[[ResolveReceiver]](key, receiver) needs a key? IMO, this operation should be about what object should the engine use for a certain operation, not about what operation is the engine about to performance on that object.
The resolve
proxy hook uses the key, hence why it's there. As for why that key could be useful, I stated an example in the initial comment (emphasis mine):
A proxy might wish to delegate to two different targets, depending on the key. Instead of defining a very weird and complicated
get
andset
, it could choose to implement getOwnPropertyDescriptor to return the relevant descriptor, resolve to return the relevant target as the receiver, and ownKeys to return the two sets of keys merged together.
That's one example where the key would be useful. This proposal was designed to enable more than simple membranes, but also be flexible enough for things like emulate multiple delegation without actually adding that to the language. Just giving the building blocks for it is all that's necessary here.
- why do you think this is something observable from outside? Why do you need
Reflect.resolve
?
That method was primarily to be consistent with all the other methods. I'm not married to the idea personally and I removed it from the main proposal, if that helps.
- I'm curious about the new proxy invariant that you're suggesting. Why? perf? remember that the proxy handler is mutable (a mistake from the past that we keep regretting), so I wonder why is this such a big problem?
The invariant is to be consistent with getOwnPropertyDescriptor
- when calling non-configurable own methods, you'd expect both the descriptor and the receiver to remain the same on each call. Anything else would seem extremely bizarre, which is why I banned it.
I'm not married to the idea of passing the key, so I'm okay with that getting removed, in which it basically becomes #4.
@isiahmeadows, thanks for the explanation. I think I can narrow the proposal now. #4 propose introducing a new mechanism that is generic, it is not about proxies only, the unwrapping mechanism can potentially be used in other scenarios. From that point of view, the mechanics to achieve the unwrap should not take into consideration neither the proxy itself or keys, or anything else other than the object to be unwrapped. And yes, proxies will tap into those mechanism to support proxy unwrapping.
The invariant is to be consistent with
getOwnPropertyDescriptor
I need to think more about this and talk to @erights on Thur about this particular question. At first glance unwrap seems harmless, similar to shadow target mechanism, but there might be something that I'm not seeing here.
@caridy As an alternative, you can just make it so when accessing descriptors for non-configurable properties, you only call unwrap
the first time for each key and memoize it as part of the descriptor, never to call it again for that key. That avoids the need to check the invariant while still remaining consistent with getOwnPropertyDescriptor
, and it's probably faster. It also works better with #4 by avoiding having to expose nearly as much behavior around the key.
Originally suggested here.
Edit: /cc @caridy (Forgot to ping you about this. My bad.)
Edit 2: Remove some less useful parts.
My thought is this: we could add a new essential internal method and corresponding proxy hook to resolve the base
this
used to invoke getters, setters, and methods. It would be invoked on each call with the key and receiver, so it can determine the properthis
to invoke the method with.this
value, namely the outer proxy instance itself. The return value is the receiver used to invoke the getter, setter, or method.handler.resolve(target, key, receiver)
, where the target is what you'd expect, and the key and receiver are as specified above. If missing, the default implementation is to unconditionally return the receiver without further action.resolve
twice: one inside [[Get]] and one for invoking the method itself.get
is missing.resolve
must return the same value on each call whenkey
remains the same.And necessarily, [[Get]] and [[Set]] have to be updated to properly handle resolving receivers:
Proxies implementing
resolve
to return the target are not recommended to expose themselves as subtypable because of all the edge cases around proper subtyping, but they may choose to, and the proposal admits this possibility.This would address the problem with membranes via the following:
this
.In code, this is how you make a proxy a membrane:
It is also probably the most flexible, as it lends itself well to a few other possibilities:
get
andset
, it could choose to implementgetOwnPropertyDescriptor
to return the relevant descriptor,resolve
to return the relevant target as the receiver, andownKeys
to return the two sets of keys merged together.super
could potentially be converted into a relatively simplearguments
-like exotic object while retaining all of its existing behavior. This is not actually being proposed here (it's out of scope), but it's a possible extension that could be done using this.Details about `super` being defined as an exotic object
This isn't actually being proposed currently, but you could in theory convert `super` within classes into an `arguments`-like construct this way. Note that `super` as defined below would be generated per-call, just like `arguments` is today. Fields for each `super` exotic object *O*: - [[Environment]] is the lexical environment *O* originated from. - *O*.[[Prototype]] is set to the active prototype used. Within static methods, this is set to the value used within the `extends` clause, but within instance methods, it's the value's `prototype` property. - *O*.[[Extensible]] is set to `false`. - No extra data properties are set, so the object is effectively an empty, frozen object. - Note: all of these properties are constant. For each instance `super` exotic object *O*, all the essential internal methods are as specified for ordinary objects, except *O*.[[ResolveReceiver]] is set as per below: - *O*.\[[ResolveReceiver]](*target*, *key*, *receiver*) 1. Return *O*.[[Environment]].GetThisBinding(). For each constructor `super` exotic object *O*, [[SuperConstructor]] is the parent superclass, the value used within the `extends` clause, all the internal fields and methods are set as for instance `super` exotic objects, and [[Call]] is defined as below: - *O*.\[[Call]](*ignored*, *args*): 1. Let *env* be *O*.[[Environment]]. 1. If ! *env*.HasThisBinding() is `true`, throw a `TypeError` exception. 1. Return *env*.BindThisValue(? Construct(*O*.[[SuperConstructor]], *env*.[[NewTarget]], *args*)). In proxy form, where `env` is the environment, `Parent` is the superclass, and `proto` is the `super` base, here's what it'd roughly look like: ```js new Proxy(Object.preventExtensions(Object.create(proto)), { apply(target, thisArg, args) { if (env.this != null) throw new TypeError("`super` has already been called") env.this = Reflect.construct(Parent, env["new.target"], args) return env.this }, resolve(target, key, receiver) { if (env.this == null) throw new TypeError("`this` is not yet initialized") return env.this }, }) ``` Doing this would mean `super` would no longer need very much special treatment from the spec. Things like [IsSuperReference](https://tc39.github.io/ecma262/#sec-issuperreference), [[[HomeObject]]](https://tc39.github.io/ecma262/#table-16), and [GetSuperBase](https://tc39.github.io/ecma262/#table-17) would no longer need to exist. - IsSuperReference would become irrelevant - duck typing is sufficient. - [[HomeObject]] is the [[Prototype]] for `super` exotic objects above. - GetSuperBase would become irrelevant - [[ResolveReceiver]] would fill in that functionality. - [MakeSuperPropertyReference](https://tc39.github.io/ecma262/#sec-makesuperpropertyreference) would be replaced with just setting up `super` appropriately when invoking a method, at the same time as `this` and `arguments`. Most of the existing syntactic rules around `super` would be simplified to just define `super` as similar to `new.target` or `this`. And the lexical stuff would treat `super` as similar to `this`.If you're curious about the performance implications of this, engines can drop ICs to detect proxies and whether they set
resolve
. If they do, it's an obvious candidate for inlining most of the time, and can be inlined like any other proxy method. In bytecode, it might be slower if engines don't check for the "return the first/third argument" pattern, but I doubt that'll be a major problem even if not implemented. (The issue only exists for proxies.)