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
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)