Skip to main content

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 value
  • writable: Whether the value can be changed
  • enumerable: 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 property
  • set: Function that serves as a setter for the property
  • enumerable: Same as above
  • configurable: 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

  1. Create an object with a read-only property called id and a writable property called name.
  2. Implement a Circle object with a radius property and computed area and circumference properties using getters.
  3. Create a Person object with firstName and lastName properties, and a computed fullName property that can be both read and written.
  4. Build a Counter object with private _count property and methods to increment, decrement, and get the current count.
  5. 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!