Advanced JavaScript (ES6+)

Getters, Setters, and Descriptors

13 min Lesson 31 of 40

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:

  1. Private properties for price and discount
  2. A getter for finalPrice that calculates price after discount
  3. A setter for discount that only allows values between 0 and 100
  4. 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!