Thank you for the reply, it explains a lot. The link to the object invariants is very useful, I was not aware of those. I quickly read the invariants of (generic) [[Get]] and they make sense. It is now also clear that the Proxy invariants are more or less consequences of the generic invariants.
The approach seems to be:
-
non-configurable + non-writable (from now on: "frozen") nature of a property is the owning object's business. An inheriting object of course may override its value or other attributes;
-
if a Proxy is created on a target, the "proxified" behavior only concerns the direct target, not its prototype chain, as also supported by the fact that [[GetPrototypeOf]] applied to the Proxy object by default returns the same value as [[GetPrototypeOf]] applied to the target (and this is another invariant for non-extensible targets). In other words, the Proxy object "substitutes" the target but not its prototype chain.
-
the generic invariants (those that apply to all objects) must somehow be translated to invariants of Proxy objects in such a way that the translated Proxy invariants enforce the generic invariants no matter what the handler does, which is difficult since the behavior of the handler is not controlled by the underlying implementation.
However, the Proxy object is a new object that is not "equal to" or "same as" the target object. Therefore, the way the [[Get]] invariant for frozen properties is translated to an invariant of the Proxy [[Get]] is imperfect (as you put it: "either remember every value previously returned, which is a lot of work, or simply enforce that in cases such as this the behavior of the Proxy corresponds to the behavior of the target [...] Obviously the spec goes the second route." I get it, this is a simplification for the sake of the possibility of actual implementation. Nevertheless, this is a stronger (more limiting) invariant than strictly necessary, so not a perfect translation of the generic invariant (a perfect translation would be your first option). However, such imperfection seems to be unavoidable. All in all, this settles (1).
With respect to (2), it also OK. But the situation is a bit more complicated here. If what has been said so far is true then the TypeError thrown for p.b
makes sense, because "b"
acts as an own property of p
(the Proxy object) and the value returned by handler.get()
violates the invariant above. So far so good. However,
(3): Reflect.getOwnPropertyDescriptor(p,"b")
returns {value:"bar.b", ...}
, which feels awkward. Shouldn't this also invoke handler.get()
and throw?
In addition: essentially "a"
also acts as an own property of p
(because the handler overrides the value that comes from the prototype chain, so property "a"
, as observed by a client of the Proxy object, is definitely not a property of either foo
or bar
, so "it must be" a property of p
). Still it is not reported as such (e.g. by Reflect.getOwnPropertyDescriptor(p,"a")
etc.).
This, however, may be a problem with the handler implementation, as it does not report "effective" property "a"
. So maybe the handler is wrong, it should implement handler.ownKeys()
and handler.getOwnPropertyDescriptor()
and report property "a"
? Let's try to do that (also removing Object.freeze, which was a mistake as it triggers another, irrelevant, invariant):
const handler = {
get(target,property) {
return `proxy:${Reflect.get(target,property)}`;
},
ownKeys(target) {return Reflect.ownKeys(target).concat(["a"]);},
getOwnPropertyDescriptor(target,property) {
if (property === "a") {
return {writable:false, configurable:false, enumerable:false, value:this.get(target,property)};
}
return Reflect.getOwnPropertyDescriptor(target,property);
}
};
const foo = Object.create(null);
Reflect.defineProperty(foo,"a",{value:"foo.a"});
const bar = Object.create(foo);
Reflect.defineProperty(bar,"b",{value:"bar.b"});
const p = new Proxy(bar,handler);
console.log(p.a); // "proxy:foo.a" -- OK, by (2), overriding `"a"` is allowed
console.log(Reflect.getOwnPropertyDescriptor(p,"a")); // TypeError -- oops! (4)
console.log(p.b); // TypeError -- OK, by (1), overriding `"b"` is not allowed
console.log(Reflect.getOwnPropertyDescriptor(p,"b")); // {value:"bar.b", ...} -- surprise! (3)
(3): should not this throw also?
(4): another invariant kicked in, I do not quite get it.
To sum it up: if we have frozen data properties on the target object and its prototype chain, then
- a direct property cannot be overridden in the proxy, yet the property descriptor describes it with the original value,
- an inherited property can be overridden in the proxy, yet the property descriptor cannot be reported by the proxy.
Seems like overriding frozen data properties consistently with proxies is kind of impossible either way.