preventSetters()
freezes an object (as per Object.freeze()
) and
additionally alters it so its setters throw.
import {preventSetters,areSettersBlocked} from "object-prevent-setters";
const u = new URL( "https://example.com/index.html" );
preventSetters( u );
areSettersBlocked(u) === true;
u.protocol = "http:"; // ***THROWS****
Modern javascript objects use setters (on the prototype) to define
properties. So Object.freeze()
is largely ineffectual:
const u = new URL( "https://example.com/" );
Object.freeze( u );
u.protocol = "http:"; // This works!
u.toString() === "http://example.com/"
It's for this reason we have abominations like DOMRectReadOnly
.
Contrast this to, say, C++ where you can just do const URL url = new URL("https://example.com");
and know the URL is unwriteable.
preventSetters()
takes a step towards this by re-writing the setters
(including those on the prototype) so they honour a hidden flag on the
object which blocks setting.
It's not perfect. But if a class's methods use its own setters, then you will get readonly protection:
import {preventSetters} from "object-prevent-setters";
class Pt {
#x = 0;
get x() { return this.#x }
set x(value) { this.#x = value }
translate( dx ) {
// Note `this.x` and _not_ `this.#x`
this.x += dx;
}
}
const p = new Pt, q = new Pt;
Object.freeze(p);
p.translate( 1 );
p.x === 1;
preventSetters( q );
q.translate( 1 ); //< **throws**
npm i object-prevent-setters
There's just a single file main.mjs which can be called from the
browser. It exports two functions: preventSetters()
and
areSettersBlocked()
object
Anything.- Return:
object
This takes an object
and rewrites its own setters, and all the setters
on its prototype chain, so that they throw when used on this object.
It then freezes the object (with Object.freeze()
).
In common with Object.freeze()
, non-object values are ignored. For
convienece, object
is returned.
object
anything.- Return:
boolean
Returns true iff preventSetters()
has been called on the object.
Non-objects return false.
-
This is trivial to break; for example:
const date = new Date(0); assert.equal( date.getUTCHours(), 0 ); preventSetters( date ); assert.doesNotThrow( () => date.setUTCHours(1) ); assert.equal( date.getUTCHours(), 1 );
(Issue: should we special case date to block this? Are there any other obvious classes where we might apply this?)
But more subtle cases are possible. For example:
const u = new URL( "https://example.com/index.html" ); preventSetters( u ); assert.throws( () => u.search = "?some=thing" ); u.searchParams.append( "some", "thing" ); assert.equal( u.toString(), "https://example.com/index.html?some=thing" );
-
Setters can be inserted onto the object's prototype after
preventSetters()
has been called and they won't be blocked. -
A private field is added to objects passed to
preventSetters()
, in addition to its setters being rewritten, and that changes the "shape" of the object - which will hurt performance in most engines. It means objects which have and haven't been throughpreventSetters()
will have different "shapes" so there's concealed polymorphism. -
The entire prototype chain of an object is monkey-patched with all the setters being rewritten, and each prototype object then has the private field added. Suffice to say, peformance will suffer. But if this proves popular, who knows TC39 may decide to implement it for real. ;)
My recommendation would be:
- Only use
preventSetters()
on your own classes. - call
preventSetters()
immediately after the declaraion of a class; for example:This means the changes to the prototype happen before most classes are instanced. I might add a class decorator, once decorators become mainstream.class C { // ... }; preventSetters( new C );
- 1.0.1 Fixed some typos in README.md (the examples used the old repository name).