In JavaScript, what does each of Object.preventExtensions(), Object.seal() and Object.freeze() block on an object, and how do the three relate to one another?
answer
- three widening locks on one object
- add, delete, reconfigure, write
- each level contains the previous
- only own properties are touched
- freeze also kills writable
basics
~10 sObject.preventExtensions blocks adding properties; Object.seal also blocks deleting and reconfiguring them; Object.freeze additionally blocks writing existing values. Each level includes the previous one, and all three are shallow — nested objects stay mutable.
solid answer
~40 sThey are three increasing levels of locking one object. `Object.preventExtensions(obj)` marks the object non-extensible: no new properties can be added and its prototype can no longer be replaced, but existing properties can still be written, deleted and reconfigured. `Object.seal(obj)` does that **and** marks every own property non-configurable, so you can no longer delete properties or change their descriptors — writable data properties can still be assigned. `Object.freeze(obj)` does everything seal does **and** makes every own data property non-writable, so existing values are locked too. The predicates `Object.isExtensible`, `Object.isSealed` and `Object.isFrozen` report each state. All three act only on the object's own properties: `Object.freeze({ nested: {} })` leaves `nested` fully mutable, which is why people write a recursive deep-freeze helper.
code
javascript · 13 linesconst p = Object.preventExtensions({ a: 1 });
p.b = 2;
console.log('b' in p, delete p.a, p.a); // false true undefined
const s = Object.seal({ x: 1 });
console.log(delete s.x); // false
s.x = 2;
console.log(s.x); // 2 - still writable
const f = Object.freeze({ y: 1, nested: { z: 2 } });
f.y = 9;
f.nested.z = 9;
console.log(f.y, f.nested.z, Object.isFrozen(f)); // 1 9 truego deeper
Be able to name the three functions and say plainly what each one blocks: adding, then also deleting/reconfiguring, then also writing. Say out loud that freeze is shallow before you are asked.
Explain them in descriptor terms — non-extensible plus configurable: false plus writable: false — and know that the isFrozen/isSealed predicates test state rather than which function ran.
Show where each belongs in real code: frozen exported constants and startup config, sealed fixed-shape records, and be ready to say why freezing a Map or a class with private fields buys you nothing.
Own the policy question: whether the codebase gets its immutability guarantees from freezing at runtime, from a copy-on-write discipline, or from types, and what each choice costs in allocation, debuggability and enforcement.
## The three operations as one ladder JavaScript objects carry two kinds of lockable state: an object-level `[[Extensible]]` flag, and per-property attributes (`writable`, `enumerable`, `configurable`). The three built-ins turn these off in increasing amounts, and each one is a strict superset of the one before it. ### 1. `Object.preventExtensions(obj)` — no new properties Sets `[[Extensible]]` to `false`. From then on: - adding a property fails (silently in sloppy mode, `TypeError` in strict mode); - replacing the prototype fails — `Object.setPrototypeOf` on a non-extensible object throws a `TypeError`; - **deleting** an existing property still works; - **writing** an existing property still works; - **reconfiguring** an existing property still works. ```js const o = Object.preventExtensions({ a: 1 }); o.b = 2; // no new property delete o.a; // true — deletion is still allowed Object.isExtensible(o); // false ``` ### 2. `Object.seal(obj)` — no new, no delete, no reconfigure Seal calls `preventExtensions` and then sets `configurable: false` on every own property. `configurable` is the attribute that governs deletion and descriptor changes, so sealing removes both: ```js const s = Object.seal({ x: 1 }); delete s.x; // false in sloppy mode, TypeError in strict s.x = 2; // allowed — writable is untouched ``` One subtlety: a non-configurable property may still be flipped from `writable: true` to `writable: false` (the only descriptor transition the spec permits once `configurable` is gone). That is exactly how `freeze` is defined on top of `seal`. ### 3. `Object.freeze(obj)` — also no writes Freeze does everything seal does, plus sets `writable: false` on every own **data** property. Accessor properties keep their getter and setter: a frozen object whose `x` is an accessor still runs the setter on `obj.x = 1`, because freeze cannot delete a setter, only make the property non-configurable. ```js const f = Object.freeze({ y: 1 }); f.y = 9; f.y; // 1 Object.isFrozen(f); // true ``` ## The predicates `Object.isExtensible`, `Object.isSealed` and `Object.isFrozen` ask about the current *state*, not about which function was called. An object you built by hand with non-configurable, non-writable properties and `preventExtensions` reports `isFrozen === true`. Two consequences catch people out: - An empty non-extensible object is both sealed and frozen — there are no properties left to violate the condition: `Object.isFrozen(Object.preventExtensions({}))` is `true`. - Since ES2015 these functions accept non-objects: `Object.isFrozen(5)` is `true` and `Object.freeze(5)` returns `5` instead of throwing (ES5 threw a `TypeError` for a non-object argument). ## Shallow, always All three operate on the object's *own* properties only. Nothing recurses: ```js const cfg = Object.freeze({ db: { host: 'localhost' } }); cfg.db.host = 'evil'; // works — cfg.db was never frozen ``` The reference stored in `cfg.db` cannot be replaced, but the object it points at is untouched. Deep immutability requires walking the graph yourself. Data that lives in internal slots rather than in properties is also unaffected: freezing a `Map`, a `Set`, or an instance with `#private` fields does not stop `map.set(...)` or a method from mutating `#count`, because those are not properties at all. ## Arrays Arrays are objects, so all three apply, and the array `length` property is what makes the result feel different. A frozen array has non-writable indices and a non-writable `length`, so `push` fails. Notably `Array.prototype.push` throws a `TypeError` even in sloppy mode, because the algorithm performs a throwing `Set`; a bare `arr[0] = 1` on the same array fails silently in sloppy mode. A *sealed* array still allows `arr[0] = 1` but not `push` or `pop` changing the element count. ## `const` is a different thing `const x = {}` freezes the *binding*: you cannot reassign `x`. It says nothing about the object's properties. `Object.freeze` is the mirror image: the properties are locked, the variable holding the reference is not. Answering "use `const`" to an immutability question is the classic mistake. ## When each is used In practice `Object.freeze` is by far the most used — locking exported constants, enum-like lookup tables, and configuration read at startup, so that an accidental write is either a no-op or a loud failure. `Object.seal` shows up when you want a fixed shape but mutable values (a settings record whose keys must not grow). `Object.preventExtensions` alone is rare and mostly appears as an implementation detail of the other two.
- Which of the three, if any, stops someone from replacing the object's prototype?All three, because sealing and freezing both begin by making the object non-extensible, and `[[SetPrototypeOf]]` refuses on a non-extensible object. `Object.setPrototypeOf(frozenObj, other)` throws a `TypeError`, and assigning through the `__proto__` setter throws as well regardless of strict mode. Note this only protects the link, not the prototype object itself — that object stays mutable unless you freeze it too.
- Why does Object.isFrozen return true for an object you only passed to Object.preventExtensions?Because `isFrozen` tests the resulting state, not the call history: an object is frozen if it is non-extensible and no own property is configurable or writable. If the object has no own properties, that condition is vacuously satisfied, so `Object.isFrozen(Object.preventExtensions({}))` is `true`. Add one ordinary property first and it becomes `false`.
- Does freezing an object with a getter and setter prevent the setter from running?No. Freeze can only make an accessor property non-configurable — it cannot remove the accessor, and `writable` does not apply to accessors. `obj.x = 1` on a frozen object still invokes the setter, and whatever that setter mutates elsewhere (a closure variable, a WeakMap, another object) changes as usual. Only data properties genuinely become read-only.
preventExtensions locks the front door so nobody new moves in; seal also welds the room dividers in place; freeze additionally bolts the furniture down — but the boxes inside the furniture are still open.
saying these in an interview costs you the question
- Saying Object.freeze makes the whole object graph immutable
- Claiming const makes the object's properties unchangeable
- Thinking seal blocks assignment to existing properties
- Believing preventExtensions stops deletion of properties
- Assuming freeze also protects Map or Set contents