What survives
Published
Frozen configuration objects and sealed singletons are load-bearing in a way most code is not: they are the thing other code trusts. Measured against the real engine in five configurations, freezing, sealing and property attributes all came through identical. One configuration does change the answer, and it changes it silently, which is why it is worth naming precisely.
Why immutability looks like it should break
Freezing an object is a promise made at one point in a program and relied on everywhere else. A frozen configuration object is passed around on the understanding that nobody can edit it. A sealed registry is handed to plugins on the understanding that they cannot add keys to it. The value of the pattern is entirely in the guarantee, so a transform that weakened the guarantee without producing an error would be worse than one that crashed.
That concern is sharper here than in most areas, because the failure mode is quiet. If a rename breaks a method call you get a TypeError and a stack trace. If a write to a read-only property starts succeeding, you get no signal at all. The program keeps running, and something that was supposed to be constant is now a variable.
The measured answer is that the ordinary uses of this family are untouched, and that there is exactly one shape where the guarantee moves. Knowing which shape it is turns the question from a reason to avoid protection into a one-line review item.
What was measured
A single sample was written to exercise the surface in one file: Object.freeze with a write, an addition and a delete attempted against it; Object.isFrozen, Object.isSealed and Object.isExtensible; Object.seal with a permitted write, a rejected addition and a rejected delete; Object.preventExtensions with an addition rejected and a delete allowed; Object.defineProperty creating a non-writable property and a non-enumerable one; Object.keys against Object.getOwnPropertyNames; a descriptor read; the strict-mode case where writing to a frozen property throws; a frozen array rejecting push; and a frozen object whose method still runs.
The file was run in Node, then protected and run again, with the two outputs compared line by line. Five configurations were used: the default ES5 target, the modern target, a realistic renaming build with identifier replacement and global renaming, that same build on the modern target, and a string-table build with MoveStrings and EncodeStrings enabled.
All five matched the original exactly. A frozen object still reported true from isFrozen and still held its value after a sloppy-mode write. Sealing still allowed writes to existing keys while rejecting additions and deletes. preventExtensions still allowed the delete that sealing forbids, which is the distinction between the two that is easiest to get wrong by hand. The strict-mode write to a frozen property still threw a TypeError, and the frozen array still threw on push while keeping its contents.
Why it holds: literal keys and dot access move together
Renaming a property is not one operation but a set of coordinated rewrites, and the reason ordinary frozen objects are safe is that the coordination covers the two forms this pattern uses.
A frozen configuration object is almost always built from an object literal and then read with dot access. Both of those are rename sites, and the transform rewrites them as a pair. In the protected output of the sample, the literal passed to Object.freeze came out with its keys renamed, and every dot access that read those keys came out renamed to match. The object is frozen just as before; only the names differ, and they differ consistently.
The functions themselves are never the issue. Object.freeze, Object.seal and Object.getOwnPropertyDescriptor are properties of a built-in object, and by default the member transform is not enabled, so those calls are untouched. The booleans they return are values rather than names.
This is also why the result is stable across configurations. Neither the ES5 rewrite nor the string table introduces a new place where a property name would be treated differently, so all five builds agree.
The one shape that quietly changes: defineProperty by string
There is a shape where the guarantee moves, and it is worth stating exactly, because it is invisible rather than loud.
Object.defineProperty takes its property name as an ordinary string argument. A string argument to a function call is not a rename site. Dot access is. So if a property is defined by Object.defineProperty(obj, "ro", { writable: false }) and then written with obj.ro = 99, member renaming rewrites the write and leaves the definition alone. Measured, with a pattern scoped to the sample's own names, the emitted code read Object.defineProperty(obj, "ro", ...) followed by obj._0x4 = 99.
The consequence is precisely the quiet one described above. The write no longer targets the read-only property; it creates a new, ordinary, writable property beside it on an object that was never made non-extensible. The read that follows returns 99 rather than the protected 5, Object.keys reports both names, and a property defined as non-enumerable becomes unreachable by the dot access that was meant to read it, returning undefined.
This is the member transform working as documented rather than a fault in it: a name given as a string to a function is data, and the transform does not rewrite data. It is the same asymmetry that makes hasOwnProperty("kind") go stale under renaming. The rule that follows is narrow and easy to apply: if you use Object.defineProperty, Object.getOwnPropertyDescriptor or any string-keyed reflection on your own objects, either keep those names out of your member pattern or list them in the exclusion list, so the definition and the access stay in agreement.
Freezing is shallow, and protection does not change that
One property of this family is worth restating because it is regularly mistaken for a protection problem when it is a language behaviour.
Object.freeze is shallow. It freezes the object you hand it and nothing below it. In the sample, a frozen object with a nested child still accepted a write to that child, before protection and after it, in all five configurations. If you are relying on a frozen tree, you need a recursive freeze, and that is true of unprotected code as well.
The same applies to the sloppy-mode silence. Outside strict mode, a write to a frozen property fails without complaint; inside strict mode it throws. That difference is a mode difference and travels with the directive, not with protection. It is worth knowing which mode your protected bundle actually runs in, because a lost directive turns a loud failure into a quiet one, and that question has its own page.
Neither of these is a reason to avoid protection. They are reasons to be precise about what freezing was ever promising.
What this does not buy you
Freezing an object is an integrity control inside your own program. It is not a secrecy control, and protection does not turn it into one.
A frozen configuration object is still fully readable by anyone running the page. They can log it, expand it in a console, or read it out of memory. Freezing prevents your own code, and other code on the page, from editing it by accident or on purpose; it does not hide the values. If the object holds something that must remain unknown to the user, freezing and protection together still leave it in the browser, and the answer is to keep it on a server instead.
It is also worth being honest about what freezing protects against on a hostile page. An attacker who controls the page can work around a frozen object in ways your own code would not, so a frozen object raises the cost of casual tampering rather than settling the question. That is a real gain, and it is a smaller claim than the one people sometimes make for it.
Scope, and how to check your own build
This article covers Object.freeze, Object.seal, Object.preventExtensions and property attributes set through Object.defineProperty, measured on the default configuration and the four variants named above, plus a member-renaming column driven with a pattern scoped to the sample's own property names. It does not cover proxies, which intercept access rather than fix it, and which have their own article.
Checking your own build takes a few minutes and is more useful than reasoning about it. Write a scratch file that freezes or seals the objects your project actually freezes, attempts the writes you care about, and prints the result of each attempt along with Object.keys. Protect it with the configuration your project really uses, including your member pattern if you have one, run both files under Node, and compare.
If you use a member pattern, the check worth running specifically is the one this article turns on: print Object.getOwnPropertyNames after the writes. Two names where you expected one is the signature of a definition and an access that stopped agreeing.
Frequently asked questions
Does obfuscation break Object.freeze?
No. Measured across five configurations, including the default ES5 target and a realistic renaming build, a frozen object still reported frozen, still rejected writes, additions and deletes, and still threw a TypeError on a strict-mode write. Object literal keys and dot access are renamed as a pair, so a frozen object built from a literal and read with dot access stays consistent.
Do sealed objects and preventExtensions behave the same after protection?
Yes. Sealing still permitted writes to existing properties while rejecting additions and deletes, and preventExtensions still rejected additions while permitting deletes. That distinction between the two is preserved, which matters because it is the part most often confused.
Is there any case where protection weakens a read-only property?
One, and it is worth reviewing for. Object.defineProperty takes the property name as a string argument, which is not a rename site, while dot access is. With member renaming enabled and a pattern matching that name, the definition keeps the original name and the access is rewritten, so a write creates a new writable property instead of failing against the read-only one. Keep such names out of the member pattern or in the exclusion list.
Why is that case dangerous if nothing crashes?
Because nothing crashes. A rename that breaks a method call gives you a TypeError and a stack trace. A write that silently lands on a new property gives you no signal, and a value your program treats as constant becomes editable. That is why the check to run is printing the object's own property names after the write rather than watching for an exception.
Does protection make a frozen object deeply frozen?
No, and neither does the language. Object.freeze is shallow both before and after protection: a nested child object still accepted writes in every configuration measured. If you need a frozen tree you need a recursive freeze, and that is unchanged by protection.
Does freezing an object hide its values from users?
No. Freezing is an integrity control within your own program, not a secrecy control, and protection does not change that. A frozen configuration object is still readable by anyone running the page. Anything that must stay unknown to the user belongs behind an API call rather than in the bundle.
Related reading