Object Property Descriptors in JavaScript
January 4, 2026
Object Property Descriptors in JavaScript
Object Property Descriptors allow you to configure and control the behavior of object properties in JavaScript. They provide fine-grained control over how properties can be accessed, modified, and enumerated, enabling you to create more robust and secure objects.
What are Property Descriptors?
A property descriptor is an object that describes the attributes of a property. It controls:
- Whether a property can be modified (
writable) - Whether a property can be deleted or reconfigured (
configurable) - Whether a property appears in enumeration (
enumerable) - The property's value (
value) or getter/setter functions
Property Descriptor Attributes
1. value
The value associated with the property. Default: undefined
2. writable
If false, the property value cannot be changed. Default: false for descriptors created with Object.defineProperty(), true for normal properties
3. enumerable
If true, the property will be enumerated in for...in loops and Object.keys(). Default: false
4. configurable
If false, the property cannot be deleted or its attributes cannot be changed. Default: false
5. get (Accessor Descriptor)
A function that serves as a getter for the property. Cannot be used with value or writable
6. set (Accessor Descriptor)
A function that serves as a setter for the property. Cannot be used with value or writable
Defining Property Descriptors
Single Property: Object.defineProperty()
const obj = {
a: "sample value",
b: 1,
c: 4,
};
// Configure a single property
Object.defineProperty(obj, "a", {
enumerable: false, // Property won't show up in for...in loops
});
// Get descriptor for a property
console.log(Object.getOwnPropertyDescriptor(obj, "a"));
// Output:
// {
// value: 'sample value',
// writable: true,
// enumerable: false, // Changed to false
// configurable: true
// }
Multiple Properties: Object.defineProperties()
const obj = {
a: "sample value",
b: 1,
c: 4,
};
// Configure multiple properties at once
Object.defineProperties(obj, {
b: {
enumerable: false, // Won't show in loops
},
c: {
enumerable: true,
configurable: false, // Cannot be deleted or reconfigured
},
});
// Get descriptors for all properties
console.log(Object.getOwnPropertyDescriptors(obj));
Property Descriptor Examples
Example 1: Non-Enumerable Property
const obj = {
publicProp: "I'm public",
};
Object.defineProperty(obj, "privateProp", {
value: "I'm private",
enumerable: false,
});
// Only enumerable properties appear
for (let key in obj) {
console.log(key); // Only "publicProp"
}
console.log(Object.keys(obj)); // ["publicProp"]
console.log(obj.privateProp); // "I'm private" - still accessible!
Example 2: Non-Writable Property
const obj = {};
Object.defineProperty(obj, "readOnly", {
value: "Cannot change me",
writable: false,
});
obj.readOnly = "New value"; // Silent failure in non-strict mode
console.log(obj.readOnly); // "Cannot change me"
// In strict mode, this would throw an error
Example 3: Non-Configurable Property
const obj = {};
Object.defineProperty(obj, "locked", {
value: "I'm locked",
configurable: false,
});
// Cannot delete
delete obj.locked; // Returns false, property still exists
console.log(obj.locked); // "I'm locked"
// Cannot reconfigure
Object.defineProperty(obj, "locked", {
enumerable: false, // Error: Cannot redefine property
});
Example 4: Complete Configuration
const obj = {};
Object.defineProperty(obj, "fullyConfigured", {
value: "Initial value",
writable: true, // Can be modified
enumerable: true, // Shows in loops
configurable: true, // Can be deleted/reconfigured
});
// Later, you can change it
Object.defineProperty(obj, "fullyConfigured", {
value: "New value",
writable: false, // Now it's read-only
enumerable: false, // Now it's hidden
configurable: false, // Now it's locked
});
Accessor Descriptors (Getters and Setters)
Instead of value and writable, you can use get and set:
const obj = {
_temperature: 0,
};
Object.defineProperty(obj, "temperature", {
get() {
return this._temperature;
},
set(value) {
if (value < -273.15) {
throw new Error("Temperature cannot be below absolute zero");
}
this._temperature = value;
},
enumerable: true,
configurable: true,
});
obj.temperature = 25;
console.log(obj.temperature); // 25
obj.temperature = -300; // Error in strict mode
Computed Properties with Getters
const rectangle = {
width: 10,
height: 5,
};
Object.defineProperty(rectangle, "area", {
get() {
return this.width * this.height;
},
enumerable: true,
configurable: true,
});
console.log(rectangle.area); // 50
rectangle.width = 20;
console.log(rectangle.area); // 100 (automatically recalculated)
Real-World Use Cases
1. Creating Constants
const config = {};
Object.defineProperty(config, "API_URL", {
value: "https://api.example.com",
writable: false,
enumerable: true,
configurable: false,
});
// config.API_URL = "new url"; // Won't work
2. Hiding Internal Properties
class BankAccount {
constructor(balance) {
this._balance = balance;
// Hide internal balance from enumeration
Object.defineProperty(this, "_balance", {
enumerable: false,
writable: true,
configurable: false,
});
}
get balance() {
return this._balance;
}
}
const account = new BankAccount(1000);
console.log(Object.keys(account)); // ["balance"] - _balance is hidden
3. Validation with Setters
function createValidatedObject() {
const obj = {
_age: 0,
};
Object.defineProperty(obj, "age", {
get() {
return this._age;
},
set(value) {
if (typeof value !== "number" || value < 0 || value > 150) {
throw new Error("Invalid age");
}
this._age = value;
},
enumerable: true,
configurable: true,
});
return obj;
}
const person = createValidatedObject();
person.age = 25; // OK
// person.age = -5; // Error
4. Property Observation
function createObservable(obj, property, callback) {
let value = obj[property];
Object.defineProperty(obj, property, {
get() {
return value;
},
set(newValue) {
const oldValue = value;
value = newValue;
callback(newValue, oldValue);
},
enumerable: true,
configurable: true,
});
}
const user = { name: "John" };
createObservable(user, "name", (newVal, oldVal) => {
console.log(`Name changed from ${oldVal} to ${newVal}`);
});
user.name = "Jane"; // Logs: "Name changed from John to Jane"
Getting Property Descriptors
Single Property
const obj = { name: "John" };
Object.defineProperty(obj, "age", {
value: 30,
writable: false,
enumerable: true,
configurable: true,
});
const descriptor = Object.getOwnPropertyDescriptor(obj, "age");
console.log(descriptor);
// {
// value: 30,
// writable: false,
// enumerable: true,
// configurable: true
// }
All Properties
const descriptors = Object.getOwnPropertyDescriptors(obj);
console.log(descriptors);
// {
// name: { value: "John", writable: true, enumerable: true, configurable: true },
// age: { value: 30, writable: false, enumerable: true, configurable: true }
// }
Default Values
When using Object.defineProperty(), if you don't specify attributes, they default to false:
const obj = {};
Object.defineProperty(obj, "prop", { value: 42 });
// Defaults:
// writable: false
// enumerable: false
// configurable: false
When creating properties normally, they default to true:
const obj = { prop: 42 };
// writable: true
// enumerable: true
// configurable: true
Data Descriptors vs Accessor Descriptors
Data Descriptor
- Has
valueand optionallywritable - Cannot have
getorset
Accessor Descriptor
- Has
getand/orset - Cannot have
valueorwritable
// Data descriptor
Object.defineProperty(obj, "data", {
value: 42,
writable: true,
});
// Accessor descriptor
Object.defineProperty(obj, "accessor", {
get() {
return this._value;
},
set(v) {
this._value = v;
},
});
Best Practices
- Use for Constants: Create truly constant values
- Hide Implementation: Make internal properties non-enumerable
- Add Validation: Use setters for input validation
- Computed Properties: Use getters for derived values
- Observable Patterns: Implement property change notifications
- Security: Lock down properties that shouldn't be modified
Summary
Property descriptors give you powerful control over object properties:
writable: Controls if a property can be modifiedenumerable: Controls if a property appears in iterationsconfigurable: Controls if a property can be deleted/reconfiguredget/set: Create computed or validated properties
Understanding property descriptors is essential for creating robust, secure, and well-designed JavaScript objects.