Object.unfreeze, or similar API
You can just do:
const proxy = new Proxy(obj, { set() { throw new Error(); }, defineProperty() { throw new Error();}, deleteProperty() { throw new Error(); } }) this.emit('some:event', proxy)
Though, it seems like an exotic use case.
That's actually very useful, thanks! I have plenty of cases where I wished I could pass an object without cloning it to ensure it isn't mutated from outside. And instead of creating the Proxy right when I will emit an event (for example), I could just store the proxy somewhere so it is long-lived and to avoid GC.
So, what if there was a way to unfreeze an object in the scope in which the object was frozen?
I don't think the behavior of object operations should depend on the scope in which they are used.
And I could understand undoing [[PreventExtensions]], just switch [[Extensible]] back to true (for ordinary objects). But unfreezing makes no sense. How is ES supposed to know which properties became non-configurable because of Object.freeze
and which ones were manually defined as non-configurable? Do you want all non-configurable properties to become configurable when "unfreezing"? I think that would be bad.
// recommend all listeners to be synchronous, and not modify the payload later
You can recommend, but what if they are not synchronous? They will have a reference to the unfrozen object! If you trust them, you can avoid freezing in the first place. If you can't trust them, unfreezing is a big problem!
Frankly I don't see the point, if you don't want to clone just use a proxy that only allows restricted access.
let error = () => { throw new Error(); };
let allowed = ["get", "ownKeys", "has", "getOwnPropertyDescriptor"];
let handler = new Proxy(Object.create(null), {
get(_, trap, receiver) {
if (!allowed.includes(trap)) return error;
}
});
this.emit('some:event', new Proxy(obj, handler));
Note that if you want to use the Proxy that way you will probably want to implement the setPrototypeOf and preventExtensions traps as well since, together with set, defineProperty and deleteProperty, they are the traps whose associated internal methods can have side effects on the target when the traps are not implemented.
The entire purpose of Object.freeze is that the frozen object can never be altered again (in the ways that freeze restricts, at least). Allowing an object to be unfrozen would violate that very critical security property.
The same is true with seal and preventExtensions. Once locked down, an object must not be unlockable.
Hello Oriol, why did you make two Proxies there? Does it serve some purpose not achieved with only one Proxy?
I used a 2nd proxy as the handler in order to only allow the desired traps.
Sure, I could have defined all blacklisted traps in an ordinary object, and make them throw an error.
But this wouldn't be future-proof in case new traps are eventually added. Whitelisting is safer.
I see, so you're relying on the engine reading the handler object during
the moment that the user of my obj
will try to read obj
, so if the
engine tries to read something in the handler (that we haven't whitelisted)
due to what the user is doing with obj
then we throw the error. If the
engine were to read these all in advance, this would cause a problem. Are
we sure that all engines don't read the handler spec in advance?
If the engine were to read these all in advance, this would cause a problem.
No, my handler always provides the same function for a given trap name, so even if the traps were cached when the proxy is constructed, the code would still work.
Are we sure that all engines don't read the handler spec in advance?
Yes, the spec does not allow this. But there was a proposal about doing that in some cases, in order to improve performance.
I think the ability to unfreeze an object can be useful.
For example, suppose we have a library that emits events:
this.emit('some:event', this.obj)
Maybe we want to try to prevent external listeners from modifying the event payload without having to clone
this.obj
, otherwise cloning can cause "jank" from GC during animations that should be smooth.So, what if there was a way to unfreeze an object in the scope in which the object was frozen?
Object.freeze(obj) // instead of cloning this.emit('some:event', this.obj) // recommend all listeners to be synchronous, and not modify the payload later Object.unfreeze(this.obj) // in the same scope
or maybe it return an unfreeze function:
const unfreeze = Object.freeze(obj) // instead of cloning this.emit('some:event', this.obj) // recommend all listeners to be synchronous, and not modify the payload later unfreeze(this.obj) // only code that is given the unfreeze function can call it, similar to resolve/reject functions with Promises
or maybe it is more similar to setTimeout and setInterval:
const frozenKey = Object.freeze(obj) // instead of cloning this.emit('some:event', this.obj) // recommend all listeners to be synchronous, and not modify the payload later Object.unfreeze(frozenKey) // only code that is given the key can unfreeze the object associated with the key
It seems like there can be opportunity for this to be optimized, so that it can be faster than cloning. For example, this uses more memory and causes GC:
this.emit('some:event', { ...this.obj }) // creates a new object
Could the performance benefit make it worth it to have a way to unfreeze objects?
Another use case is unlocking things only when used in certain places, as a "guard". In the following example, the only way to set the X rotation value is with the setter, which enforces that certain logic will fire, rather than someone from the outside modifying the readable state not using the setter:
import Privates from 'somewhere' // a private cache, using WeakMap internally const _ = new Privates class ThingThatRotates { constructor() { this.rotation = { x: 0, y: 0, z: 0 } _(this).frozen = Object.freeze(this.rotation) // ... } get rotateX() { return this.rotation.x } set rotateX(val) { this.doSomethingImportantWithTheValue(val) // unlock here, so that the value can only be set with this setter. Object.unfreeze(_(this).frozen) this.rotation.x = val _(this).frozen = Object.freeze(this.rotation) this.emitAnEventWithTheValue(val) this.etc(val) } // ... }
I know, I can probably just keep
rotation
in the private cache, but then it isn't readable. Maybe I want that to be readable as a convenient shortcut. F.e.// outside code const r = new ThingThatRotates // X rotation might be animated by some other tool const {x, y, z} = r.rotation // for convenience, no cloning involved.
This is a completely contrived example I just made up right now. I can probably think of more. It seems like it could be good for performance in cases where we don't want modification from the outside...
/#!/JoePea