This is followup on making the global object's [[Prototype]] chain immutable. It was agreed that a proposal to extend this to be a full proxy trap, so that ordinary objects and arrays and so on could have immutable [[Prototype]], would be entertained, and I'd write up the spec-proposal for it. Said spec-proposal isn't complete yet, but I have an approximate start on it thus far.
Right now the gist of the idea is to add a [[SetImmutablePrototype]] trap and (on most objects) a [[MutablePrototype]] Boolean internal slot indicating mutability (initially true). I have this completed, generally sensibly enough, except for a few issues still. I'll post my current, un-landable progress after filing this issue and figuring out how to attach that progress.
Yay!
on most objects) a [[MutablePrototype]] Boolean internal slot indicating mutability (initially true).
I think this woudl be better integrated into [[Extensible]]. So instead of being a boolean, [[Extensible]] becomes a tri-state "extensible", "immutable prototype", "non-extensible". It isn't possible to have an object that is non-existensible but with a mutable prototype, so a two-boolean design gives too many degrees of freedom.
In order to add a proxy trap, a corresponding Reflect method would need to be added. I suspect this would need to be part of a full proposal and maybe wouldn't be able to be just a PR.
Agree with @Domenic, s/[[Extensible]]/[[Extensibility]] and tri-state seems good.
https://github.com/jswalden/ecma262/tree/setimmutableprototype is the current quasi-state of what I have. It adds the extra [[MutablePrototype]] thing, rather than the [[Extensibility]] thing mooted already. The issue is that it only adds _one_ trap, [[SetImmutablePrototype]], right now. But another is needed (or so it seems) to expose whether the [[Prototype]] is immutable. See the XXX bits in the branch above (and tell me how to attach its current state here, rather than literally just linking it). _Does_ it seem like two additional traps are required here, or is there some other better approach for this?
I'd like to revive this thread, given its connection with https://github.com/tc39/proposal-class-fields/issues/179 .
Could we do this without a separate couple traps, and instead repurposing preventExtensions, passing an options bag indicating that we're just talking about the prototype? In the future, maybe this trap could also be used with a different flag for "deep freezing".
Imagine that it would look like this:
let obj = {}
Object.isExtensible(obj); // true
Object.isExtensible(obj, { proto: true }); // true
Object.preventExtensions(obj, { proto: true });
obj.property = 1; // Works
obj.__proto__ = {}; // throws in strict mode
Object.isExtensible(obj); // true
Object.isExtensible(obj, { proto: true }); // false
Object.preventExtensions(obj);
obj.property = 1; // throws in strict mode
obj.__proto__ = {}; // throws in strict mode
Object.isExtensible(obj); // false
Object.isExtensible(obj, { proto: true }); // false
On the Proxy side, the options bag could be passed as a second argument to the preventExtensions and isExtensible traps.
When running code that expects to be able to use this options bag when the JavaScript engine or Proxy doesn't support it, the observed behavior would only be more conservative: In the preventExtensions call, it would do even more freezing, and in the isExtensible call, it would return non-extensible in fewer cases. For this reason, from an ocap security perspective, I hope it would be considered safe. (A deep-freeze flag would not be conservative in the same way, for example.)
I don't know how other engines work, but the way V8 implemented immutable prototype exotic objects already supports objects transitioning into an immutable prototype state (though maybe caching of the map should be implemented if we go this way); I don't think the Proxy or Reflect changes will be particularly taxing to implement. But of course we should think this through and be confident that this is a good design.
cc @erights @allenwb
I'm in favor of bringing this back too; thanks for starting the discussion, @littledan.
Could we do this without a separate couple traps
Out of curiosity, what's the motivation for this? I suspect we might be able to, but new traps seem easier to me, to spec correctly, to use correctly, and to feature-detect.
the observed behavior would only be more conservative: In the
preventExtensionscall, it would do even more freezing, and in theisExtensible call, it would return non-extensible in fewer cases
How so? It looks like it wouldn't do _more_ freezing; rather it would do _different_ freezing. Freezing the properties but not the prototype when you intended to freeze the prototype but not the properties seems undesirable.
Out of curiosity, what's the motivation for this? I suspect we might be able to, but new traps seem easier to me, to spec correctly, to use correctly, and to feature-detect.
I'd be fine with separate traps as well; I just got the sense that it'd be annoying to add two extra Proxy traps. Maybe, when we bring this to the committee, we can give them the multiple options to bikeshed over as well. I don't have a strong opinion here. Agree with you about option bags not being as easily feature-testable, but I'm not sure that means we shouldn't add options.
How so? It looks like it wouldn't do more freezing; rather it would do different freezing. Freezing the properties but not the prototype when you intended to freeze the prototype but not the properties seems undesirable.
This would be "conservative" in the sense that, if you have an old browser which doesn't support the options bag, it would silently do more freezing. And, for objects which are just prototype-frozen, like Object.prototype, it would report that they are non-extensible.
It seems like a more robust approach is throwing when the desired functionality isn鈥檛 present - which separate methods/traps provides.
If we overloaded, I鈥檇 expect the safer fallback to be freeze, not preventExtensions, because then it freezes more than intended (instead of less).
@ljharb Note that making the prototype immutable is even less strong than preventExtensions. No one has proposed a MOP operation for freeze in this thread.
That's very true - the only current operation that makes the prototype immutable is Object.freeze, so if we went with an options bag, I'd want the object to be guaranteed to make the prototype immutable, and make freezing the keys the backwards-compatible side effect.
the only current operation that makes the prototype immutable is
Object.freeze
Also Object.seal and Object.preventExtensions
Right, preventExtensions is our MOP operation to freeze the prototype, and passing the options bag would do a strict subset of what that operation does.
ah, my mistake :-) i'd forgotten that all of them freeze the prototype as well; in which case preventExtensions would be the right place to add the option.
That leaves this opinion:
It seems like a more robust approach is throwing when the desired functionality isn鈥檛 present - which separate methods/traps provides.
By throwing when it's not in place, you mean like Object.freezePrototype would throw as undefined is not a function?
Yup! That also (as mentioned earlier in the thread) makes feature detection and polyfilling much simpler.
What's the use case for immutable-prototype-but-not-sealed?
@zenparsing for one, it would make Object.prototype no longer need to be exotic.
@zenparsing This could let a class always call its parent constructor, while continuing to let additional static methods be monkey-patched on, as discussed in https://github.com/tc39/proposal-class-fields/issues/179
Most helpful comment
Yay!
I think this woudl be better integrated into [[Extensible]]. So instead of being a boolean, [[Extensible]] becomes a tri-state "extensible", "immutable prototype", "non-extensible". It isn't possible to have an object that is non-existensible but with a mutable prototype, so a two-boolean design gives too many degrees of freedom.