JavaScript · Core language
Objects, descriptors, getters/setters
⚡ TL;DR — Every property is really a descriptor: either a data descriptor (
value,writable) or an accessor descriptor (get/set), each carryingenumerableandconfigurableflags.obj.x = 1just hides all of this by defaulting every flag totrue;Object.definePropertygives you the control the literal syntax conceals.
🧠 Mental model
A property is not a value — it's a small record of attributes. Assigning obj.x = 1 creates a data property with writable/enumerable/configurable all true. Getters and setters are simply accessor descriptors: a property backed by functions that run on read/write, yet indistinguishable from a plain field at the call site — you write obj.x, never obj.x(). That transparency is the point, and also the trap.
| Data descriptor | Accessor descriptor |
|---|---|
value, writable |
get, set |
enumerable, configurable |
enumerable, configurable |
You cannot mix value with get/set on one property.
⚙️ How it actually works
enumerablecontrols visibility infor…in,Object.keys, spread, andJSON.stringify.writable: falsemakes assignment silently fail in sloppy mode and throw in strict mode.configurable: falseis a one-way door — you can no longer delete the property or change its flags (the sole exception: flippingwritablefromtruetofalse).Object.definePropertydefaults every unspecified flag tofalse— the exact opposite of literal syntax. This is the number-one "why doesn't my property show up inObject.keys?" bug.
Object.freeze, seal, and preventExtensions are all built on these flags.
💻 Code
const user = {};
Object.defineProperty(user, 'id', {
value: 42,
writable: false, // read-only
enumerable: false, // hidden from Object.keys / JSON / spread
configurable: false, // can't be deleted or reconfigured
});
user.id = 99; // sloppy: silently ignored; strict: TypeError
Object.keys(user); // [] — id is non-enumerable
// Accessor: validation + a derived, virtual property
const temp = {
_c: 0,
get fahrenheit() { return this._c * 9 / 5 + 32; },
set celsius(v) {
if (v < -273.15) throw new RangeError('below absolute zero');
this._c = v;
},
};
temp.celsius = 100;
temp.fahrenheit; // 212 — computed on read, looks like a plain field
⚖️ Trade-offs
- Getters/setters buy clean APIs — validation on write, computed props on read — but they hide cost: a getter can look like a field yet run expensive work on every access, and every
{...obj}orJSON.stringifyinvokes them. - Non-enumerable / non-configurable props are excellent for library internals and constants, but overusing them makes objects opaque and hard to debug.
Object.freezeis a correctness guard, not a deep one — it's shallow, so nested objects stay mutable; deep immutability needs recursion or a library.- When NOT to reach for descriptors: for app-level encapsulation, a class with
#privatefields reads far clearer thandefinePropertygymnastics.
💣 Gotchas interviewers probe
definePropertydefaults flags tofalse. Define a property the "manual" way and it's silently non-enumerable/non-writable/non-configurable. Literal syntax defaults them all totrue.- Getters run on every access — logging, spread, and
JSON.stringifyall trigger them, so a side-effecting or throwing getter surfaces in surprising places. Object.freezeis shallow.freeze({a:{b:1}})still lets you set.a.b.- Spread /
Object.assigncopy only enumerable own properties and evaluate getters — they copy the returned value, not the accessor itself. configurable: falsecan't be undone — you can't delete, redefine, or re-enable it.- Setter without getter makes reads return
undefined; getter without setter makes writes silently no-op (sloppy) or throw (strict). __proto__is itself an accessor defined onObject.prototype— which is whyObject.create(null)is safer for dictionaries.
🎯 Say this in the interview
"Under the hood every property is a descriptor — either a data descriptor with
valueandwritable, or an accessor descriptor withget/set— plusenumerableandconfigurableflags. Literal assignment just creates a data property with all flags true, which is why people forget the flags exist. The gotcha I always flag is thatObject.definePropertydefaults every unspecified flag to false, so a manually defined property is non-enumerable and won't show up inObject.keysorJSON.stringifyunless you opt in. Getters and setters are accessor descriptors: they look like plain fields at the call site, which is great for validation and computed values but dangerous because they run on every access — including spread and serialization. And I rememberObject.freezeis shallow, so nested objects still mutate."
🔗 Go deeper
- javascript.info — Property flags and descriptors — every flag and the
definePropertydefaults. - javascript.info — Property getters and setters — accessor descriptors in depth.
- MDN —
Object.defineProperty— exact attribute semantics. - MDN —
Object.freeze— including its shallow behaviour.