Property Descriptors
TL;DR
When you add a property to an object (user.name = "Alice"), it doesn’t just store the value "Alice". It creates a hidden Property Descriptor—a configuration object that dictates whether the property can be changed, deleted, or looped over.
Mental Model
How It Works
By default, standard assignment creates properties where writable, enumerable, and configurable are all true.
However, using Object.defineProperty(), you can customize these flags to create robust, read-only API contracts.
writable: Iffalse, doingobj.prop = 5silently fails (or throws an error in Strict Mode).enumerable: Iffalse, the property becomes invisible tofor...inloops andObject.keys().configurable: Iffalse, the property cannot be deleted (delete obj.prop), and you cannot change these descriptor flags again.
Example
const user = {};
// We manually define a property instead of using user.id = 1
Object.defineProperty(user, "id", {
value: 999,
writable: false, // Cannot be changed!
enumerable: false, // Will hide from loops!
configurable: false // Cannot be deleted!
});
user.name = "Alice"; // Normal property
console.log(user.id); // 999
// 1. Writable test
user.id = 1000; // Fails silently (or throws in strict mode)
console.log(user.id); // Still 999!
// 2. Enumerable test
console.log(Object.keys(user)); // ["name"] (id is completely hidden!)
// 3. Configurable test
delete user.id; // Fails silently
console.log(user.id); // Still 999!
Common Output Question
const math = {
PI: 3.14
};
Object.freeze(math);
math.PI = 4;
console.log(math.PI);
Q: What does Object.freeze() actually do under the hood, and what is the output?
A: Output is 3.14.
Object.freeze() is essentially a convenience wrapper around Property Descriptors. Under the hood, it loops through every property on the object and sets writable: false and configurable: false for all of them, making the entire top-level of the object completely immutable.
Senior Interview Question
Q: How do Getters and Setters fit into Property Descriptors?
A: A Property Descriptor comes in two flavors: Data Descriptors and Accessor Descriptors. They are mutually exclusive.
A Data Descriptor contains value and writable.
An Accessor Descriptor contains get() and set() functions.
You cannot define a property that has both a value and a get() method. Getters/Setters allow you to execute logic (like validation or computing derived state) every time a property is read or written to, masquerading as a normal property assignment.