JavaScript - Getters, Setters, and Private Properties
This guide introduces you to the concepts of getters, setters, and private properties in JavaScript, explaining their use cases, syntax, and benefits, along with practical examples.
Introduction to Getters and Setters
What are Getters and Setters?
Imagine you have a car. The car has various attributes like color, speed, and model. You might want to control how these attributes are accessed or set. In JavaScript, getters and setters are like the keys to accessing and modifying these attributes in a controlled way. Getters are like the odometer in a car that lets you check the speed, without allowing you to directly change it. Setters are like the accelerator which allows you to set the speed but within certain limits.
Why Use Getters and Setters?
Getters and setters provide a layer of control over the properties of an object. They allow you to execute a function whenever a property is get or set, enabling the addition of logic to the read and write operations. This can be useful for data validation, transformation, and encapsulation, ensuring that the internal state of an object remains consistent and protected.
Defining Getters and Setters
Getters
Getters allow you to define a method that can be accessed like a property. When a getter is called, the associated function is executed, and its return value is returned.
Basic Syntax
A getter is defined using the get
keyword followed by a function name and parentheses. The parentheses are required but should not contain any parameters.
const obj = {
get propertyName() {
// Function body
}
}
Example of a Getter
Let's create an object that represents a circle. The circle has a radius and a method to calculate the area.
const circle = {
radius: 10,
get area() {
return Math.PI * Math.pow(this.radius, 2);
}
};
console.log(circle.area); // Output: 314.1592653589793
In this example, the area
is a getter that calculates the area of the circle whenever it is accessed.
Multiple Getters
You can define multiple getters for different properties.
const person = {
firstName: "John",
lastName: "Doe",
get fullName() {
return `${this.firstName} ${this.lastName}`;
},
get initials() {
return `${this.firstName[0]}.${this.lastName[0]}`;
}
};
console.log(person.fullName); // Output: John Doe
console.log(person.initials); // Output: J.D
Here, the fullName
getter combines the first name and last name, while the initials
getter returns the initials.
Setters
Setters are methods that allow you to define rules for setting the value of a property.
Basic Syntax
A setter is defined using the set
keyword followed by a function name and parentheses, which must contain one parameter.
const obj = {
set propertyName(newValue) {
// Function body
}
}
Example of a Setter
Let's enhance our circle example by adding a setter for the radius
property to ensure it remains positive.
const circle = {
_radius: 10,
get radius() {
return this._radius;
},
set radius(newRadius) {
if (newRadius > 0) {
this._radius = newRadius;
} else {
console.error("Radius can't be negative");
}
},
get area() {
return Math.PI * Math.pow(this._radius, 2);
}
};
circle.radius = 15;
console.log(circle.radius); // Output: 15
console.log(circle.area); // Output: 706.8583470577034
circle.radius = -5; // Output: Radius can't be negative
console.log(circle.radius); // Output: 15 (unchanged)
In this example, the radius
setter checks if the new radius is greater than zero before setting it, preventing invalid values.
Multiple Setters
You can define multiple setters for different properties.
const person = {
_firstName: "John",
_lastName: "Doe",
get firstName() {
return this._firstName;
},
set firstName(newFirstName) {
if (typeof newFirstName === 'string') {
this._firstName = newFirstName;
} else {
console.error("First name must be a string");
}
},
get lastName() {
return this._lastName;
},
set lastName(newLastName) {
if (typeof newLastName === 'string') {
this._lastName = newLastName;
} else {
console.error("Last name must be a string");
}
}
};
person.firstName = "Jane";
console.log(person.firstName); // Output: Jane
person.lastName = "Smith";
console.log(person.lastName); // Output: Smith
person.firstName = 123; // Output: First name must be a string
console.log(person.firstName); // Output: Jane (unchanged)
This example shows how to use setters to enforce data validation.
Using Getters and Setters in Classes
Classes provide a structured way to define objects and their behaviors. Getters and setters can be particularly useful within classes to manage object properties.
Defining Getters and Setters in Class Declarations
Here’s how you can define getters and setters within classes.
Example Using Getters and Setters
Let's create a Rectangle
class with width and height properties. We'll add getters and setters to control how width and height are accessed and modified.
class Rectangle {
constructor(width, height) {
this._width = width;
this._height = height;
}
get width() {
return this._width;
}
set width(newWidth) {
if (newWidth > 0) {
this._width = newWidth;
} else {
console.error("Width can't be negative");
}
}
get height() {
return this._height;
}
set height(newHeight) {
if (newHeight > 0) {
this._height = newHeight;
} else {
console.error("Height can't be negative");
}
}
get area() {
return this._width * this._height;
}
}
const rect = new Rectangle(5, 10);
console.log(rect.area); // Output: 50
rect.width = 8;
console.log(rect.area); // Output: 80
rect.width = -3; // Output: Width can't be negative
console.log(rect.width); // Output: 8 (unchanged)
In this code, the Rectangle
class has getters and setters for width
and height
to ensure they remain positive. The area
is a getter that calculates the area based on the current width and height.
Accessing and Modifying Properties
Getters and setters allow you to control how properties are accessed and modified.
Accessing Getter Properties
Accessing a getter is straightforward, just like accessing a regular property.
const person = {
_firstName: "John",
_lastName: "Doe",
get fullName() {
return `${this._firstName} ${this._lastName}`;
}
};
console.log(person.fullName); // Output: John Doe
In this example, fullName
is a getter that returns the full name of the person.
Modifying Properties with Setters
Modifying a property through a setter is like setting a regular property, but it triggers the setter logic.
const person = {
_firstName: "John",
_lastName: "Doe",
get firstName() {
return this._firstName;
},
set firstName(newFirstName) {
if (typeof newFirstName === 'string') {
this._firstName = newFirstName;
} else {
console.error("First name must be a string");
}
},
get lastName() {
return this._lastName;
},
set lastName(newLastName) {
if (typeof newLastName === 'string') {
this._lastName = newLastName;
} else {
console.error("Last name must be a string");
}
}
};
person.firstName = "Jane";
console.log(person.firstName); // Output: Jane
person.lastName = "Smith";
console.log(person.lastName); // Output: Smith
person.firstName = 123; // Output: First name must be a string
console.log(person.firstName); // Output: Jane (unchanged)
Benefits of Using Getters and Setters in Classes
- Data Validation: You can enforce rules when a value is set, preventing invalid data.
- Encapsulation: You can hide internal state and expose only what is necessary.
- Abstraction: You can provide a simple interface for accessing and modifying complex properties.
Introduction to Private Properties
What are Private Properties?
Private properties are properties that are inaccessible from outside the class they are defined in. They are like a secret compartment in a car that only the car's owner can access.
Why Use Private Properties?
Private properties help protect the integrity of your data by preventing external code from directly modifying the internal state of an object. This is crucial for maintaining the consistency and robustness of your application.
Defining Private Properties in JavaScript
#
Syntax
Using the JavaScript introduced private properties in ES2022 using the #
symbol.
Basic Syntax
A private property is defined using the #
symbol before the property name.
class ClassName {
#_privatePropertyName;
}
Example of a Private Field
Let's define a class BankAccount
with a private #balance
field.
class BankAccount {
#balance = 1000;
get balance() {
return this.#balance;
}
set balance(newBalance) {
if (newBalance >= 0) {
this.#balance = newBalance;
} else {
console.error("Balance can't be negative");
}
}
}
const account = new BankAccount();
console.log(account.balance); // Output: 1000
account.balance = 1500;
console.log(account.balance); // Output: 1500
account.balance = -500; // Output: Balance can't be negative
console.log(account.balance); // Output: 1500 (unchanged)
In this BankAccount
class, the #balance
field is private and can only be accessed or modified through the balance
getter and setter.
Multiple Private Fields
You can define multiple private fields in a class.
class Employee {
#firstName;
#lastName;
constructor(firstName, lastName) {
this.#firstName = firstName;
this.#lastName = lastName;
}
get firstName() {
return this.#firstName;
}
set firstName(newFirstName) {
if (typeof newFirstName === 'string') {
this.#firstName = newFirstName;
} else {
console.error("First name must be a string");
}
}
get lastName() {
return this.#lastName;
}
set lastName(newLastName) {
if (typeof newLastName === 'string') {
this.#lastName = newLastName;
} else {
console.error("Last name must be a string");
}
}
get fullName() {
return `${this.#firstName} ${this.#lastName}`;
}
}
const employee = new Employee("John", "Doe");
console.log(employee.fullName); // Output: John Doe
employee.firstName = "Jane";
console.log(employee.fullName); // Output: Jane Doe
employee.lastName = "Smith";
console.log(employee.fullName); // Output: Jane Smith
employee.firstName = 123; // Output: First name must be a string
console.log(employee.firstName); // Output: Jane (unchanged)
This Employee
class uses private fields #firstName
and #lastName
to store the names and provides public access and modification through getters and setters.
Accessing Private Properties
Attempting to Access Directly
You cannot access private properties directly from outside the class.
class Circle {
#radius;
constructor(radius) {
this.#radius = radius;
}
}
const circle = new Circle(10);
console.log(circle.#radius); // SyntaxError: Private field '#radius' must be declared in an enclosing class
Attempting to access a private property directly results in a syntax error.
Modifying Private Properties
Attempting to Modify Directly
Similarly, you cannot modify private properties directly.
class Circle {
#radius;
constructor(radius) {
this.#radius = radius;
}
}
const circle = new Circle(10);
circle.#radius = 15; // SyntaxError: Private field '#radius' must be declared in an enclosing class
Trying to modify a private property directly also results in a syntax error.
Using Private Properties with Getters and Setters
Example Combining Getters, Setters, and Private Fields
Let's combine getters, setters, and private fields to create a more complex class with encapsulation.
Defining Class with Private Fields
In this example, we'll create a Product
class with private fields for #name
and #price
. We will use getters and setters to manage these properties.
class Product {
#name;
#price;
constructor(name, price) {
this.#name = name;
this.#price = price;
}
get name() {
return this.#name;
}
set name(newName) {
if (typeof newName === 'string') {
this.#name = newName;
} else {
console.error("Name must be a string");
}
}
get price() {
return this.#price;
}
set price(newPrice) {
if (newPrice > 0) {
this.#price = newPrice;
} else {
console.error("Price must be positive");
}
}
}
const product = new Product("Laptop", 1000);
console.log(product.name); // Output: Laptop
console.log(product.price); // Output: 1000
product.name = "Gaming Laptop";
console.log(product.name); // Output: Gaming Laptop
product.price = 1500;
console.log(product.price); // Output: 1500
product.name = 123; // Output: Name must be a string
console.log(product.name); // Output: Gaming Laptop (unchanged)
product.price = -500; // Output: Price must be positive
console.log(product.price); // Output: 1500 (unchanged)
This Product
class encapsulates the internal state of the name
and price
fields, providing control over how they are accessed and modified.
Benefits of Using Private Properties with Getters and Setters
- Data Encapsulation: Private properties prevent external code from directly accessing or modifying internal state.
- Data Validation: You can enforce business logic when properties are set.
- Improved Security: Provides a barrier against accidental or malicious changes.
Encapsulating Data with Getters, Setters, and Private Fields
Importance of Encapsulation
Encapsulation is a fundamental principle of object-oriented programming that helps in bundling the data and methods that operate on the data, and restricting access to some of the object's components. This prevents the accidental modification of data.
Implementing Encapsulation with Getters, Setters, and Private Fields
Example Demonstrating Encapsulation
Let's create a Car
class with private fields for #speed
and #color
and demonstrate encapsulation.
class Car {
#speed;
#color;
constructor(speed, color) {
this.#speed = speed;
this.#color = color;
}
get speed() {
return this.#speed;
}
set speed(newSpeed) {
if (newSpeed >= 0) {
this.#speed = newSpeed;
} else {
console.error("Speed can't be negative");
}
}
get color() {
return this.#color;
}
set color(newColor) {
if (typeof newColor === 'string') {
this.#color = newColor;
} else {
console.error("Color must be a string");
}
}
}
const car = new Car(100, "Red");
console.log(car.speed); // Output: 100
console.log(car.color); // Output: Red
car.speed = 120;
console.log(car.speed); // Output: 120
car.color = "Blue";
console.log(car.color); // Output: Blue
car.speed = -50; // Output: Speed can't be negative
console.log(car.speed); // Output: 120 (unchanged)
car.color = 123; // Output: Color must be a string
console.log(car.color); // Output: Blue (unchanged)
In this Car
class, the #speed
and #color
fields are private and can only be accessed or modified through the respective getters and setters.
Best Practices
Design Patterns
- Validation: Always validate data being set through setters.
- Computation: Use getters to perform computations that derive from the private fields.
- Encapsulation: Keep the internal details hidden and expose only what is necessary through the public API.
Error Handling and Debugging
Common Issues
Accessing Private Fields
Attempting to access private fields directly from outside the class results in a syntax error.
class Circle {
#radius;
constructor(radius) {
this.#radius = radius;
}
}
const circle = new Circle(10);
console.log(circle.#radius); // SyntaxError: Private field '#radius' must be declared in an enclosing class
Modifying Private Fields
Similarly, modifying private fields directly also results in a syntax error.
class Circle {
#radius;
constructor(radius) {
this.#radius = radius;
}
}
const circle = new Circle(10);
circle.#radius = 15; // SyntaxError: Private field '#radius' must be declared in an enclosing class
Debugging Tips
Using Developer Tools
When debugging, use JavaScript developer tools in your browser (like Chrome DevTools) to inspect instances of classes. You can view the public properties and use the console to interact with the methods, helping you understand how the getters and setters work.
Summary
Recap of Key Concepts
- Getters and Setters: Methods to get and set property values with additional logic.
- Private Properties: Fields declared with
#
that are inaccessible from outside the class. - Encapsulation: Bundling data and methods that operate on data, restricting access to some components.
Review of Benefits
- Data Validation: You can enforce business logic when properties are set.
- Encapsulation: Protects the internal state of an object, preventing unintended modifications.
- Improved Security: Provides a barrier against accidental or malicious changes.
Next Steps
Further Topics in JavaScript OOP
- Inheritance: Understanding how classes can inherit properties and methods from other classes.
- Static Methods and Properties: Learning about static members in JavaScript classes.
- Prototypes: Exploring the prototype-based nature of JavaScript.
Additional Resources
By understanding and using getters, setters, and private properties, you can write more robust, maintainable, and secure JavaScript code. These features provide powerful tools for managing object properties and encapsulating data within classes.