JavaScript Encapsulation Tutorial with Examples

Encapsulation is a key concept in object-oriented programming (OOP) that focuses on bundling the data (properties) and the methods (functions) that operate on that data into a single unit, usually a class or an object.

Encapsulation helps in controlling access to the internal state of an object by hiding its data and providing methods to interact with it. This makes your code more secure, maintainable, and modular.

In this tutorial, you’ll learn:

What is encapsulation in JavaScript?
How to implement encapsulation with objects and functions
Encapsulation using ES6 classes and private fields
Using getter and setter methods
Practical examples of encapsulation in JavaScript

1. What is Encapsulation in JavaScript?

Encapsulation refers to the practice of restricting direct access to the internal state of an object and only allowing controlled interaction through public methods. This concept allows you to:

Hide the internal state of an object (private variables).
Expose only necessary functionalities (public methods).
Protect the object’s state from unintended modification.
In JavaScript, encapsulation is typically achieved using:

Functions and closures.
ES6 classes with private fields and methods.

2. Implementing Encapsulation with Objects and Functions

Before ES6, encapsulation was commonly achieved using objects and functions. You can use closures to create private variables and expose public methods to interact with them.

Example 1: Encapsulation Using Closures

function Person(name, age) {
    // Private variables
    let _name = name;
    let _age = age;

    return {
        // Public method to access private data
        getDetails: function() {
            return `${_name} is ${_age} years old.`;
        },

        // Public method to update private data
        setAge: function(newAge) {
            if (newAge > 0) {
                _age = newAge;
            } else {
                console.log("Age must be positive.");
            }
        }
    };
}

const person = Person("Alice", 30);
console.log(person.getDetails());  // Output: Alice is 30 years old.
person.setAge(35);
console.log(person.getDetails());  // Output: Alice is 35 years old.

In this example:

The variables _name and _age are private because they are declared within the function scope.
The user can only access or modify these private variables through public methods like getDetails and setAge.
The internal state of the object is protected, and only the provided methods can interact with it.

3. Encapsulation Using ES6 Classes and Private Fields

With the introduction of ES6 classes and private fields (introduced in ES2020), JavaScript provides a more structured and native way to achieve encapsulation. Private fields are prefixed with a #, and they cannot be accessed or modified from outside the class.

Example 2: Encapsulation with ES6 Classes and Private Fields

class Person {
    // Private fields
    #name;
    #age;

    constructor(name, age) {
        this.#name = name;
        this.#age = age;
    }

    // Public method to access private fields
    getDetails() {
        return `${this.#name} is ${this.#age} years old.`;
    }

    // Public method to update private fields
    setAge(newAge) {
        if (newAge > 0) {
            this.#age = newAge;
        } else {
            console.log("Age must be positive.");
        }
    }
}

const person = new Person("Bob", 25);
console.log(person.getDetails());  // Output: Bob is 25 years old.
person.setAge(28);
console.log(person.getDetails());  // Output: Bob is 28 years old.

console.log(person.#age);  // SyntaxError: Private field '#age' must be declared in an enclosing class

In this example:

The fields #name and #age are private fields that cannot be accessed directly from outside the class.
You must use the public methods getDetails and setAge to interact with the private fields.
Attempting to access a private field like person.#age will throw a syntax error.

4. Using Getter and Setter Methods

In JavaScript, you can define getter and setter methods that provide a controlled way to access and modify the private state of an object. This is especially useful when you want to add additional logic (e.g., validation) before accessing or modifying the data.

Example 3: Using Getter and Setter Methods

class Person {
    // Private fields
    #name;
    #age;

    constructor(name, age) {
        this.#name = name;
        this.#age = age;
    }

    // Getter for name
    get name() {
        return this.#name;
    }

    // Setter for name
    set name(newName) {
        if (newName.length > 0) {
            this.#name = newName;
        } else {
            console.log("Name cannot be empty.");
        }
    }

    // Getter for age
    get age() {
        return this.#age;
    }

    // Setter for age with validation
    set age(newAge) {
        if (newAge > 0) {
            this.#age = newAge;
        } else {
            console.log("Age must be positive.");
        }
    }
}

const person = new Person("Charlie", 40);
console.log(person.name);  // Output: Charlie
console.log(person.age);   // Output: 40

person.name = "David";  // Updates the name
console.log(person.name);  // Output: David

person.age = -5;  // Output: Age must be positive.
console.log(person.age);  // Output: 40 (unchanged)

In this example:

Getter methods allow you to access the private fields without exposing them directly.
Setter methods allow you to modify the private fields while adding validation logic (e.g., checking if the new age is positive).
You can access and modify the fields using properties like person.name and person.age, but the actual fields are protected by encapsulation.

5. Practical Examples of Encapsulation in JavaScript

Example 4: Bank Account System

Let’s create a bank account system that encapsulates the account balance and only allows modification through specific methods.

class BankAccount {
    // Private fields
    #balance;

    constructor(initialBalance) {
        this.#balance = initialBalance;
    }

    // Public method to deposit money
    deposit(amount) {
        if (amount > 0) {
            this.#balance += amount;
            console.log(`Deposited: $${amount}`);
        } else {
            console.log("Deposit amount must be positive.");
        }
    }

    // Public method to withdraw money
    withdraw(amount) {
        if (amount > 0 && amount <= this.#balance) {
            this.#balance -= amount;
            console.log(`Withdrew: $${amount}`);
        } else {
            console.log("Invalid withdrawal amount or insufficient balance.");
        }
    }

    // Getter for balance (read-only access)
    get balance() {
        return this.#balance;
    }
}

const account = new BankAccount(100);
console.log(account.balance);  // Output: 100

account.deposit(50);  // Output: Deposited: $50
console.log(account.balance);  // Output: 150

account.withdraw(30);  // Output: Withdrew: $30
console.log(account.balance);  // Output: 120

account.withdraw(200);  // Output: Invalid withdrawal amount or insufficient balance.

In this example:

The #balance field is private and cannot be accessed directly.
The user can interact with the balance only through public methods like deposit, withdraw, and the getter method balance (which provides read-only access).
Encapsulation ensures that the internal state of the account (the balance) is protected from direct modification.

Example 5: Encapsulation in a Shopping Cart

Let’s create a shopping cart system that encapsulates the cart's internal items and provides methods to add, remove, and calculate the total price.

class ShoppingCart {
    #items = [];

    // Add item to the cart
    addItem(item, price) {
        this.#items.push({ item, price });
        console.log(`Added ${item} for $${price}`);
    }

    // Remove item from the cart
    removeItem(item) {
        const index = this.#items.findIndex((i) => i.item === item);
        if (index !== -1) {
            this.#items.splice(index, 1);
            console.log(`Removed ${item}`);
        } else {
            console.log(`${item} not found in the cart.`);
        }
    }

    // Calculate total price
    getTotalPrice() {
        return this.#items.reduce((total, item) => total + item.price, 0);
    }

    // Getter to list all items
    get items() {
        return this.#items.map((i) => i.item);
    }
}

const cart = new ShoppingCart();
cart.addItem("Laptop", 1200);  // Output: Added Laptop for $1200
cart.addItem("Phone", 800);    // Output: Added Phone for $800
console.log(cart.items);       // Output: ['Laptop', 'Phone']
console.log(cart.getTotalPrice());  // Output: 2000

cart.removeItem("Phone");  // Output: Removed Phone
console.log(cart.items);   // Output: ['Laptop']
console.log(cart.getTotalPrice());  // Output: 1200

In this example:

The #items array is private and cannot be accessed or modified directly.
The public methods addItem, removeItem, and getTotalPrice provide controlled access to the cart.
The internal state of the cart (items and prices) is encapsulated, ensuring that it can only be modified through the provided methods.

Conclusion

Encapsulation is a fundamental concept in JavaScript that helps in hiding internal implementation details and exposing only necessary functionalities through a clean interface.

Here’s a summary of what you’ve learned:

Encapsulation can be implemented using functions (closures) and objects.
ES6 introduced classes and private fields (prefixed with #) that provide a more structured way to achieve encapsulation.
Getter and setter methods allow controlled access to private data.
Encapsulation promotes data protection, code modularity, and easier maintenance.

By using encapsulation effectively in JavaScript, you can build more secure, robust, and maintainable applications!

Related posts

JavaScript Mixins Tutorial with Examples

JavaScript Classes Tutorial with Examples

JavaScript Inheritance Tutorial with Examples