Getters, Setters, and Descriptors
In this lesson, we'll explore JavaScript's powerful property access mechanisms: getters, setters, and property descriptors. These features give you fine-grained control over how properties behave in your objects.
Understanding Getters and Setters
Getters and setters allow you to define methods that execute when a property is accessed or modified, giving you control over property access:
Basic Getter and Setter:
const user = {
firstName: 'John',
lastName: 'Doe',
get fullName() {
return `${this.firstName} ${this.lastName}`;
},
set fullName(value) {
const parts = value.split(' ');
this.firstName = parts[0];
this.lastName = parts[1];
}
};
console.log(user.fullName); // John Doe
user.fullName = 'Jane Smith';
console.log(user.firstName); // Jane
console.log(user.lastName); // Smith
Key Concept: Getters and setters look like regular properties but execute functions behind the scenes. This allows you to compute values, validate input, or trigger side effects.
Getters and Setters in Classes
Classes make extensive use of getters and setters for encapsulation:
class Temperature {
constructor(celsius) {
this._celsius = celsius;
}
get celsius() {
return this._celsius;
}
set celsius(value) {
if (value < -273.15) {
throw new Error('Temperature below absolute zero!');
}
this._celsius = value;
}
get fahrenheit() {
return this._celsius * 9/5 + 32;
}
set fahrenheit(value) {
this.celsius = (value - 32) * 5/9;
}
}
const temp = new Temperature(25);
console.log(temp.celsius); // 25
console.log(temp.fahrenheit); // 77
temp.fahrenheit = 86;
console.log(temp.celsius); // 30
Property Descriptors
Every property in JavaScript has a property descriptor that defines its behavior. You can view and modify these descriptors:
Getting Property Descriptors:
const obj = { name: 'John' };
const descriptor = Object.getOwnPropertyDescriptor(obj, 'name');
console.log(descriptor);
// {
// value: 'John',
// writable: true,
// enumerable: true,
// configurable: true
// }
Property Descriptor Attributes
Understanding the four key attributes of property descriptors:
Descriptor Attributes:
1. value: The property's value
2. writable: Can the value be changed?
3. enumerable: Will it show in for...in loops?
4. configurable: Can the descriptor be changed or property deleted?
Tip: For accessor properties (getters/setters), the descriptor has 'get' and 'set' attributes instead of 'value' and 'writable'.
Defining Properties with Descriptors
Use Object.defineProperty() to create properties with specific descriptors:
const person = {};
Object.defineProperty(person, 'age', {
value: 30,
writable: false, // Read-only
enumerable: true,
configurable: false
});
person.age = 40; // Silently fails (strict mode: throws error)
console.log(person.age); // 30
delete person.age; // Fails because configurable is false
console.log(person.age); // 30
Creating Non-Enumerable Properties
Non-enumerable properties don't appear in loops or Object.keys():
const user = {
name: 'John',
email: 'john@example.com'
};
Object.defineProperty(user, 'password', {
value: 'secret123',
enumerable: false, // Hidden from enumeration
writable: true,
configurable: true
});
console.log(user.password); // secret123
console.log(Object.keys(user)); // ['name', 'email']
console.log(JSON.stringify(user)); // {"name":"John","email":"john@example.com"}
Computed Properties with Getters
Use getters to create computed properties that derive from other properties:
class Circle {
constructor(radius) {
this.radius = radius;
}
get diameter() {
return this.radius * 2;
}
get circumference() {
return 2 * Math.PI * this.radius;
}
get area() {
return Math.PI * this.radius ** 2;
}
}
const circle = new Circle(5);
console.log(circle.diameter); // 10
console.log(circle.circumference); // 31.41592653589793
console.log(circle.area); // 78.53981633974483
Validation with Setters
Setters are perfect for validating input before storing values:
class User {
constructor(name, age) {
this.name = name;
this.age = age;
}
set age(value) {
if (typeof value !== 'number') {
throw new TypeError('Age must be a number');
}
if (value < 0 || value > 150) {
throw new RangeError('Age must be between 0 and 150');
}
this._age = value;
}
get age() {
return this._age;
}
}
const user = new User('John', 30);
console.log(user.age); // 30
// user.age = 'thirty'; // TypeError: Age must be a number
// user.age = 200; // RangeError: Age must be between 0 and 150
Important: When using setters for validation, use a different internal property name (like _age) to avoid infinite recursion.
Private Properties Pattern
Use closures or WeakMaps with getters/setters to create truly private properties:
Using WeakMap for Privacy:
const privateData = new WeakMap();
class BankAccount {
constructor(balance) {
privateData.set(this, { balance });
}
get balance() {
return privateData.get(this).balance;
}
deposit(amount) {
if (amount <= 0) {
throw new Error('Amount must be positive');
}
const data = privateData.get(this);
data.balance += amount;
}
withdraw(amount) {
const data = privateData.get(this);
if (amount > data.balance) {
throw new Error('Insufficient funds');
}
data.balance -= amount;
}
}
const account = new BankAccount(1000);
console.log(account.balance); // 1000
account.deposit(500);
console.log(account.balance); // 1500
// No way to directly access or modify the balance
Defining Multiple Properties
Use Object.defineProperties() to define multiple properties at once:
const product = {};
Object.defineProperties(product, {
name: {
value: 'Laptop',
writable: true,
enumerable: true,
configurable: true
},
price: {
value: 999.99,
writable: false,
enumerable: true,
configurable: false
},
inStock: {
value: true,
writable: true,
enumerable: false,
configurable: true
}
});
console.log(product.name); // Laptop
console.log(product.price); // 999.99
console.log(Object.keys(product)); // ['name', 'price']
Accessor Descriptors with defineProperty
Create getters and setters using Object.defineProperty():
const rectangle = {
_width: 10,
_height: 5
};
Object.defineProperty(rectangle, 'area', {
get() {
return this._width * this._height;
},
enumerable: true,
configurable: true
});
Object.defineProperty(rectangle, 'width', {
get() {
return this._width;
},
set(value) {
if (value <= 0) {
throw new Error('Width must be positive');
}
this._width = value;
},
enumerable: true,
configurable: true
});
console.log(rectangle.area); // 50
rectangle.width = 20;
console.log(rectangle.area); // 100
Real-World Example: Form Validator
Here's a practical example using getters, setters, and validation:
class FormData {
constructor() {
this._errors = {};
}
set email(value) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(value)) {
this._errors.email = 'Invalid email format';
} else {
delete this._errors.email;
this._email = value;
}
}
get email() {
return this._email;
}
set password(value) {
if (value.length < 8) {
this._errors.password = 'Password must be at least 8 characters';
} else {
delete this._errors.password;
this._password = value;
}
}
get password() {
return this._password;
}
get isValid() {
return Object.keys(this._errors).length === 0;
}
get errors() {
return { ...this._errors };
}
}
const form = new FormData();
form.email = 'invalid-email';
form.password = 'short';
console.log(form.isValid); // false
console.log(form.errors); // { email: 'Invalid email format', password: 'Password must be at least 8 characters' }
form.email = 'user@example.com';
form.password = 'secure-password-123';
console.log(form.isValid); // true
Practice Exercise:
Task: Create a Product class with the following requirements:
- Private properties for
price and discount
- A getter for
finalPrice that calculates price after discount
- A setter for
discount that only allows values between 0 and 100
- A non-enumerable property
id
Solution:
const privateProps = new WeakMap();
class Product {
constructor(name, price, id) {
this.name = name;
privateProps.set(this, { price, discount: 0 });
Object.defineProperty(this, 'id', {
value: id,
writable: false,
enumerable: false,
configurable: false
});
}
get price() {
return privateProps.get(this).price;
}
set price(value) {
if (value < 0) {
throw new Error('Price cannot be negative');
}
privateProps.get(this).price = value;
}
get discount() {
return privateProps.get(this).discount;
}
set discount(value) {
if (value < 0 || value > 100) {
throw new RangeError('Discount must be between 0 and 100');
}
privateProps.get(this).discount = value;
}
get finalPrice() {
const { price, discount } = privateProps.get(this);
return price * (1 - discount / 100);
}
}
const laptop = new Product('Laptop', 1000, 'PROD-001');
console.log(laptop.price); // 1000
laptop.discount = 20;
console.log(laptop.finalPrice); // 800
console.log(Object.keys(laptop)); // ['name'] (id is not enumerable)
Summary
In this lesson, you learned:
- How to use getters and setters for computed and validated properties
- Property descriptors control writable, enumerable, and configurable attributes
- Object.defineProperty() and Object.defineProperties() for fine-grained control
- Creating non-enumerable and read-only properties
- Using getters/setters for validation and encapsulation
- Patterns for creating private properties with WeakMap
Next Up: In the next lesson, we'll explore ES6 Modules and modern JavaScript module systems!