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

Using the # Syntax

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.