JAVASCRIPT / OBJECTS AND PROPERTY MECHANICS
Property descriptors: value, writable, enumerable
Read and change the hidden attributes behind any object property, so you can make a property read-only or invisible to Object.keys, JSON, and spread.
What you will learn
- Read a property's four attributes with Object.getOwnPropertyDescriptor
- Predict the false defaults Object.defineProperty applies to a brand-new property
- Make a property read-only and tell silent failure apart from a strict-mode TypeError
- Hide a property from Object.keys, JSON.stringify, and spread with enumerable: false
Understanding Property descriptors: value, writable, enumerable
An own property is not simply a key pointing at a value. Internally it is a slot with four attributes: value (the data), writable (may the value be replaced through assignment), enumerable (does key-walking machinery report this key), and configurable (may the property be deleted or redefined). Object.getOwnPropertyDescriptor(obj, key) hands you a snapshot of those attributes as a plain object. Properties created by an object literal or by plain assignment get writable, enumerable and configurable all set to true, which is why ordinary objects feel like they have no attributes at all.
Object.defineProperty is the only way to set those attributes directly, and it has an asymmetry worth memorising. For a property that does not exist yet, every boolean you omit defaults to false, so { value: 1 } produces a read-only, non-enumerable, non-configurable property. For a property that already exists, a partial descriptor changes only the attributes you actually mention and leaves the rest alone. That is why Object.defineProperty(obj, 'x', { writable: false }) is a surgical lock rather than a wholesale reset.
writable: false blocks the assignment path only. In a classic script the refused write is silently discarded; in strict code, which includes every ES module and every class body, the same line throws a TypeError. Because the lock sits on the slot and not on the data, a non-writable property holding an array still lets you push into that array. enumerable: false works on a different axis: it removes the key from for...in, Object.keys/values/entries, JSON.stringify, spread and Object.assign, while direct reads, the in operator, Object.getOwnPropertyNames and Reflect.ownKeys all still see it. The language uses this itself, which is why for...in over an array does not list length or the Array.prototype methods.
The two flags are fully independent: a property can be enumerable and read-only, or writable and hidden.
const config = { host: "localhost" };
// Assignment produces writable, enumerable, configurable: all true.
console.log(Object.getOwnPropertyDescriptor(config, "host"));
// defineProperty defaults every omitted boolean to false.
Object.defineProperty(config, "version", { value: "1.4.0" });
console.log(Object.getOwnPropertyDescriptor(config, "version"));
try {
config.version = "2.0.0"; // ignored in scripts, TypeError in modules
} catch {}
console.log(config.version);
console.log(Object.keys(config), JSON.stringify(config));
console.log("version" in config, config.version);A property is a slot with attributes, not just a value, and value, writable and enumerable are independent settings you can inspect and set.
Worked examples
Hiding a property from key listing
A non-enumerable property is still fully readable, it just stops showing up wherever keys are collected.
const user = { name: "Ada" };
Object.defineProperty(user, "sessionId", {
value: "s-9f2",
writable: true,
enumerable: false,
configurable: true
});
console.log(Object.keys(user));
console.log(Object.getOwnPropertyNames(user));
console.log({ ...user });
console.log(user.sessionId);
for (const k in user) console.log("for-in:", k);Example explained
Line 1enumerable: false keeps sessionId out of Object.keys, and spread copies only enumerable own keys, so the copy loses it too.
Line 2Object.getOwnPropertyNames ignores the enumerable flag, which proves the property really is there.
Line 3user.sessionId reads normally: enumerable never gates access, only key collection.
Line 4for...in visits enumerable string keys, so it reports name and nothing else.
Read-only from birth, and strict mode
Omitting writable makes a property read-only, and strict code turns the refused write into a TypeError.
const settings = {};
Object.defineProperty(settings, "mode", { value: "dark", enumerable: true });
console.log(Object.keys(settings));
function switchMode() {
"use strict";
settings.mode = "light";
}
try {
switchMode();
} catch (err) {
console.log(err.name, "- write refused");
}
console.log(settings.mode);Example explained
Line 1Only value and enumerable were passed, so writable stayed at its default false and mode is read-only immediately.
Line 2Object.keys still lists mode because enumerable was set to true: the two attributes are independent knobs.
Line 3The "use strict" directive inside switchMode makes the refused assignment throw instead of being discarded.
Line 4settings.mode is still 'dark', because the throw replaced the write rather than following it.
Locking an existing property
A partial descriptor on an existing property changes only what it names, and configurable still permits redefinition.
const point = { x: 1, y: 2 };
Object.defineProperty(point, "x", { writable: false });
console.log(Object.getOwnPropertyDescriptor(point, "x"));
console.log(Reflect.set(point, "x", 99));
console.log(point.x);
Object.defineProperty(point, "x", { value: 42 });
console.log(point.x);Example explained
Line 1Redefining an existing property with { writable: false } leaves value, enumerable and configurable untouched.
Line 2Reflect.set returns false instead of throwing, so it reports a refused write identically in strict and non-strict code.
Line 3defineProperty can still install 42 because configurable is true; writable: false only closes the assignment path.
Important notes
A refused write is silent in a classic script and throws only in strict code, so the exact same line behaves differently depending on whether it lives in a script, a module, or a class body.
configurable is the fourth attribute: it gates delete and any further redefinition, and once it is false the only remaining change allowed is flipping writable from true to false.
Common mistakes
Treating Object.defineProperty(obj, "id", { value: 7 }) as a fancier obj.id = 7; the property comes out non-writable, non-enumerable and non-configurable, so it disappears from JSON.stringify and later assignments are dropped.
Mutating the object returned by Object.getOwnPropertyDescriptor and expecting the property to change; that object is a detached snapshot, so nothing happens until you pass it to Object.defineProperty.
Assuming writable: false protects the contents of the value; with { value: [], writable: false } you cannot reassign the property, but push still mutates the array it points to.
Try it yourself
Change, predict, then run
In a browser editor, create const book = { title: "Dune" } and attach an id with Object.defineProperty using enumerable: false and writable: false, then log JSON.stringify(book), Object.keys(book), Object.getOwnPropertyNames(book) and book.id. Confirm with Reflect.set(book, "id", 99) that the write is refused while book.id still reads fine.
Open the JavaScript workspaceCheck your understanding
After Object.defineProperty(obj, "token", { value: "abc" }) on a fresh object literal, which statement is true?
- obj.token reads "abc", but Object.keys(obj) and JSON.stringify(obj) omit it and obj.token = "xyz" leaves it unchanged
- obj.token is undefined until you also pass writable: true
- Object.keys(obj) lists "token" because it is an own property of obj
- obj.token = "xyz" throws a TypeError no matter where the code runs
Show answer
defineProperty defaults the omitted booleans to false, so the property exists and is readable ("token" in obj is true) but is skipped by every enumeration-based operation and refuses assignment. Option 4 is tempting because the write really is refused, but the TypeError only appears in strict code; in a classic script the assignment is discarded silently.