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!