Object.getOwnPropertyDescriptor and defineProperty
Every property on an object carries metadata beyond its value: whether it can be written, enumerated, or redefined. These are visible through property descriptors.
Reading a descriptor
const o = { a: 1 };
Object.getOwnPropertyDescriptor(o, "a");
// { value: 1, writable: true, enumerable: true, configurable: true }
Ordinary assignment produces exactly this descriptor — all flags on. Everything interesting happens when they are off.
Data vs accessor descriptors
A descriptor is either a data descriptor (value +
writable) or an accessor descriptor
(get + set). Mixing the groups in one
defineProperty call throws.
const obj = {};
Object.defineProperty(obj, "temp", {
get() { return this._t || 0; },
set(v) { this._t = v; },
enumerable: true,
configurable: true,
});
obj.temp = 21;
obj.temp; // 21
What each flag does
| Flag | Effect when false |
|---|---|
writable | Assignment silently fails (or throws in strict mode); value is frozen. |
enumerable | Invisible to Object.keys, for...in, and JSON.stringify. |
configurable | Cannot be deleted or have its descriptor changed — except writable true→false, one way. |
const locked = {};
Object.defineProperty(locked, "key", { value: 42, writable: false });
locked.key = 99; // throws in strict mode, ignored otherwise
locked.key; // 42
The one-way door
configurable: false is permanent. Even the object's creator
cannot undo it later:
const o = {};
Object.defineProperty(o, "x", { value: 1, configurable: false });
Object.defineProperty(o, "x", { value: 2 }); // TypeError
delete o.x; // false (strict: TypeError)
Shorthand helpers
The flags above power the family of freeze/seal helpers, useful for constant modules:
Object.freeze(obj); // writable:false + configurable:false on everything
Object.seal(obj); // configurable:false, values still writable
Object.preventExtensions(obj); // no new properties, existing ones free
Deep-freeze is not recursive — nested objects stay mutable
unless you walk them yourself.