JavaScript Property Descriptors
Introduction
When working with JavaScript objects, you're likely familiar with the standard way to create and access properties:
const user = {
name: "John",
age: 30
};
console.log(user.name); // "John"
user.age = 31;
But did you know that behind every property in a JavaScript object is a hidden set of configurations called property descriptors? These descriptors give you fine-grained control over how each property behaves.
Property descriptors allow you to:
- Make properties read-only
- Hide properties from being enumerated in loops
- Prevent properties from being deleted
- Create computed properties with getters and setters
Understanding property descriptors is a key step in mastering JavaScript objects and creating more sophisticated code.
Understanding Property Descriptors
What Are Property Descriptors?
A property descriptor is a simple object that describes how a property behaves. Each property in a JavaScript object has an associated descriptor with the following potential attributes:
Data Descriptor Attributes:
value
: The property's valuewritable
: Whether the value can be changedenumerable
: Whether the property appears in for...in loops and Object.keys()configurable
: Whether the property can be deleted or have its descriptor modified
Accessor Descriptor Attributes:
get
: Function that serves as a getter for the propertyset
: Function that serves as a setter for the propertyenumerable
: Same as aboveconfigurable
: Same as above
Examining Property Descriptors
You can view a property's descriptor using the Object.getOwnPropertyDescriptor()
method:
const person = {
name: "Emma"
};
const descriptor = Object.getOwnPropertyDescriptor(person, "name");
console.log(descriptor);
/* Output:
{
value: "Emma",
writable: true,
enumerable: true,
configurable: true
}
*/
By default, properties created through normal assignment have all boolean flags set to true
.
Modifying Property Behavior with Descriptors
Using Object.defineProperty()
The Object.defineProperty()
method allows you to create or modify a property with a custom descriptor:
const product = {};
Object.defineProperty(product, 'price', {
value: 99.99,
writable: false, // Can't change the price
enumerable: true, // Will show up in for...in loops
configurable: false // Can't delete this property or change its descriptor
});
// Try to modify the price
product.price = 79.99;
console.log(product.price); // Still 99.99, the change was ignored
// Try to delete the property
delete product.price;
console.log(product.price); // Still 99.99, deletion was ignored
Defining Multiple Properties at Once
You can use Object.defineProperties()
to define multiple properties with custom descriptors:
const car = {};
Object.defineProperties(car, {
model: {
value: 'Tesla',
writable: true
},
year: {
value: 2023,
writable: true
},
VIN: {
value: '1X2Y3Z4A5B6C',
writable: false,
enumerable: false // Hidden from loops
}
});
console.log(car.model); // "Tesla"
console.log(car.VIN); // "1X2Y3Z4A5B6C"
// VIN won't show up in this array because it's not enumerable
console.log(Object.keys(car)); // ["model", "year"]
Getters and Setters
Property descriptors also allow you to define accessor properties with custom getters and setters:
Basic Getter and Setter
const temperature = {
_celsius: 0, // Convention for "private" property
};
Object.defineProperty(temperature, 'celsius', {
get: function() {
return this._celsius;
},
set: function(value) {
if (typeof value !== 'number') {
throw new Error('Temperature must be a number');
}
this._celsius = value;
},
enumerable: true,
configurable: true
});
// Define a computed Fahrenheit property
Object.defineProperty(temperature, 'fahrenheit', {
get: function() {
return (this.celsius * 9/5) + 32;
},
set: function(value) {
if (typeof value !== 'number') {
throw new Error('Temperature must be a number');
}
this.celsius = (value - 32) * 5/9;
},
enumerable: true,
configurable: true
});
temperature.celsius = 25;
console.log(temperature.celsius); // 25
console.log(temperature.fahrenheit); // 77
temperature.fahrenheit = 68;
console.log(temperature.celsius); // 20
Getter and Setter Shorthand Syntax
For convenience, JavaScript provides a shorthand syntax for getters and setters when creating objects:
const person = {
firstName: 'John',
lastName: 'Doe',
// Getter
get fullName() {
return `${this.firstName} ${this.lastName}`;
},
// Setter
set fullName(value) {
const parts = value.split(' ');
this.firstName = parts[0];
this.lastName = parts[1];
}
};
console.log(person.fullName); // "John Doe"
person.fullName = "Jane Smith";
console.log(person.firstName); // "Jane"
console.log(person.lastName); // "Smith"
Practical Applications
Creating a Read-Only Configuration Object
const config = {};
// Define properties that cannot be changed
Object.defineProperties(config, {
API_KEY: {
value: 'abc123xyz789',
writable: false,
enumerable: true,
configurable: false
},
MAX_CONNECTIONS: {
value: 5,
writable: false,
enumerable: true,
configurable: false
}
});
// This will be ignored in strict mode or cause an error
config.API_KEY = 'try_to_hack';
console.log(config.API_KEY); // Still "abc123xyz789"
Building a Data Model with Validation
function createUser(initialData) {
const user = {};
Object.defineProperties(user, {
_name: { value: initialData.name || "", writable: true, enumerable: false },
_email: { value: initialData.email || "", writable: true, enumerable: false },
_age: { value: initialData.age || 0, writable: true, enumerable: false },
name: {
get: function() { return this._name; },
set: function(value) {
if (typeof value !== 'string') {
throw new Error('Name must be a string');
}
this._name = value;
},
enumerable: true,
configurable: true
},
email: {
get: function() { return this._email; },
set: function(value) {
if (!/^\S+@\S+\.\S+$/.test(value)) {
throw new Error('Invalid email format');
}
this._email = value;
},
enumerable: true,
configurable: true
},
age: {
get: function() { return this._age; },
set: function(value) {
const num = Number(value);
if (isNaN(num) || num < 0) {
throw new Error('Age must be a positive number');
}
this._age = num;
},
enumerable: true,
configurable: true
}
});
return user;
}
const user = createUser({ name: "Alice", email: "[email protected]", age: 28 });
console.log(user.name); // "Alice"
console.log(user.age); // 28
// This will throw an error
try {
user.age = -5;
} catch (e) {
console.log(e.message); // "Age must be a positive number"
}
// This will throw an error
try {
user.email = "invalid-email";
} catch (e) {
console.log(e.message); // "Invalid email format"
}
Creating a Logger that Records All Property Access
function createTrackedObject(obj) {
const tracked = {};
// Get all properties from the original object
const props = Object.getOwnPropertyNames(obj);
props.forEach(prop => {
// Store the original value
let value = obj[prop];
// Define a tracked property
Object.defineProperty(tracked, prop, {
get: function() {
console.log(`Accessed property: ${prop}`);
return value;
},
set: function(newValue) {
console.log(`Changed property: ${prop} from ${value} to ${newValue}`);
value = newValue;
},
enumerable: true,
configurable: true
});
});
return tracked;
}
const originalUser = {
name: "Bob",
role: "Admin",
status: "Active"
};
const trackedUser = createTrackedObject(originalUser);
console.log(trackedUser.name); // Logs: "Accessed property: name" then returns "Bob"
trackedUser.status = "Inactive"; // Logs: "Changed property: status from Active to Inactive"
console.log(trackedUser.status); // Logs: "Accessed property: status" then returns "Inactive"
Using Object.getOwnPropertyDescriptors()
To get all property descriptors for an object at once, use Object.getOwnPropertyDescriptors()
:
const car = {
make: "Toyota",
model: "Corolla"
};
// Add a non-enumerable property
Object.defineProperty(car, 'VIN', {
value: 'ABC123',
enumerable: false
});
const descriptors = Object.getOwnPropertyDescriptors(car);
console.log(descriptors);
/* Output:
{
make: {
value: "Toyota",
writable: true,
enumerable: true,
configurable: true
},
model: {
value: "Corolla",
writable: true,
enumerable: true,
configurable: true
},
VIN: {
value: "ABC123",
writable: false,
enumerable: false,
configurable: false
}
}
*/
Property Descriptor Defaults
When using Object.defineProperty()
, if you omit descriptor attributes, they default to:
value
:undefined
get
:undefined
set
:undefined
writable
:false
enumerable
:false
configurable
:false
This is different from properties created with direct assignment, which default all boolean flags to true
.
const obj = {};
// Using direct assignment
obj.prop1 = 'normal';
// Using defineProperty with empty descriptor
Object.defineProperty(obj, 'prop2', {});
console.log(Object.getOwnPropertyDescriptor(obj, 'prop1'));
/* Output:
{
value: "normal",
writable: true,
enumerable: true,
configurable: true
}
*/
console.log(Object.getOwnPropertyDescriptor(obj, 'prop2'));
/* Output:
{
value: undefined,
writable: false,
enumerable: false,
configurable: false
}
*/
Summary
Property descriptors are a powerful feature in JavaScript that give you precise control over how object properties behave:
- They allow you to control whether properties can be modified, enumerated, or deleted
- You can create computed properties with custom getter and setter functions
- Property descriptors help in building robust data models with validation
- They enable advanced patterns like read-only configurations and property access tracking
By mastering property descriptors, you gain access to a more sophisticated level of JavaScript programming, allowing you to create more secure, maintainable, and feature-rich code.
Exercises
- Create an object with a read-only property called
id
and a writable property calledname
. - Implement a
Circle
object with aradius
property and computedarea
andcircumference
properties using getters. - Create a
Person
object withfirstName
andlastName
properties, and a computedfullName
property that can be both read and written. - Build a
Counter
object with private_count
property and methods to increment, decrement, and get the current count. - Implement a simple form validation system using accessor properties for fields like email, phone number, and zip code.
Further Resources
💡 Found a typo or mistake? Click "Edit this page" to suggest a correction. Your feedback is greatly appreciated!