📖 Encapsulation in JavaScript
Encapsulation is one of the core principles of Object-Oriented Programming (OOP). It refers to the bundling of data (properties) and methods (functions) that operate on that data into a single unit, typically a class. Encapsulation also involves restricting direct access to some of the object's components, which helps to prevent the accidental modification of data.
In JavaScript, properties and methods of a class are publicly accessible by default, meaning that any code outside the class can access and modify them. However, encapsulation allows us to control how and where these properties and methods can be accessed and modified.
Public Properties and Methods
By default, all properties and methods in a JavaScript class are public, which means they can be accessed from outside the class. This is useful, but it also means that the internal state of an object can be modified in unintended ways.
class Student {
constructor(name) {
this.name = name;
}
getName() {
return this.name;
}
}
const student = new Student('John Doe');
console.log(student.name); // Outputs: John Doe
student.name = 'Jane Doe'; // Modifies the name directly
console.log(student.getName()); // Outputs: Jane Doe
In this example, the name
property is publicly accessible, which means it can be modified directly from outside the class. While this is sometimes desired, it can also lead to unexpected behavior if the property is changed in ways that the class was not designed to handle.
Private Properties and Methods
JavaScript introduced private fields with the #
syntax in ES6, allowing developers to create properties that cannot be accessed or modified directly from outside the class. This helps to protect the internal state of an object.
class Student {
#name; // Private property
constructor(name) {
this.#name = name;
}
getName() {
return this.#name;
}
}
const student = new Student('John Doe');
console.log(student.getName()); // Outputs: John Doe
console.log(student.#name); // SyntaxError: Private field '#name' must be declared in an enclosing class
In this example, the #name
property is private, meaning it cannot be accessed directly from outside the class. This helps to enforce encapsulation by ensuring that the internal state of the Student
object is protected from unintended modifications.
Access Control in Encapsulation
Encapsulation in JavaScript can also be managed through the use of getter and setter methods. These methods allow you to control how properties are accessed and modified, often including validation or additional logic.
Getters and Setters
class Student {
constructor(name) {
this._name = name; // Conventionally "protected" property
}
get name() {
return this._name;
}
set name(value) {
if (value.length > 0) {
this._name = value;
} else {
console.log('Name cannot be empty');
}
}
}
const student = new Student('Jane Doe');
console.log(student.name); // Outputs: Jane Doe
student.name = ''; // Outputs: Name cannot be empty
In this example, the name
property is accessed and modified through getter and setter methods. The setter method includes validation to ensure that the name is not set to an empty string, demonstrating how encapsulation helps to maintain the integrity of an object’s state.
Why Encapsulation Matters
- Code Maintainability
- By bundling data and methods together and restricting direct access to certain properties, encapsulation makes code easier to maintain and understand.
- Data Integrity
- Encapsulation helps to prevent accidental or intentional modifications to an object's state that could lead to bugs or inconsistent behavior.
- Abstraction
- Encapsulation supports abstraction by hiding the internal implementation details of an object and exposing only what is necessary.
Best Practices for Encapsulation
- Use private fields (
#
) - Protect sensitive data within a class by using private fields.
- Use getters and setters
- Control access to class properties and include validation logic when necessary.
- Avoid direct property access
- Interact with an object's data through methods whenever possible, rather than accessing properties directly.
Summary
- Encapsulation
- Encapsulation is the bundling of data and methods into a single unit, with access control over the internal state.
- Private Fields
- Private fields, introduced with the
#
syntax, allow for true encapsulation in JavaScript by restricting access to certain properties. - Getters and Setters
- Getters and setters provide controlled access to properties and can include validation logic.
- Best Practices
- Encapsulation helps to maintain code integrity, prevent unintended data modifications, and support abstraction.
Putting It Into Action
Encapsulation in a Bank Account Class
To see how encapsulation works in practice, let's create a simple BankAccount
class. This class will have private properties for the account balance and public methods to deposit, withdraw, and check the balance. We'll use getter and setter methods to control access to these properties.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bank Account</title>
</head>
<body>
<h1>Bank Account Management</h1>
<div id="output"></div>
<script>
class BankAccount {
#balance = 0; // Private field
deposit(amount) {
if (amount > 0) {
this.#balance += amount;
console.log(`Deposited: $${amount}`);
// output to DOM
this.outputToDOM(`Deposited: $${amount}`);
}
}
withdraw(amount) {
if (amount > 0 && amount <= this.#balance) {
this.#balance -= amount;
console.log(`Withdrew: $${amount}`);
// output to DOM
this.outputToDOM(`Withdrew: $${amount}`);
} else {
console.log('Insufficient funds or invalid amount');
// output to DOM
this.outputToDOM(message);
}
}
getBalance() {
return this.#balance;
}
outputToDOM(message) {
const outputDiv = document.getElementById('output');
const p = document.createElement('p');
p.textContent = message;
outputDiv.appendChild(p);
}
}
const account = new BankAccount();
account.deposit(100);
account.withdraw(50);
console.log(`Current Balance: $${account.getBalance()}`); // Outputs: Current Balance: $50
// Output the final balance to the DOM
const outputDiv = document.getElementById('output');
const p = document.createElement('p');
p.textContent = `Current Balance: $${account.getBalance()}`;
outputDiv.appendChild(p);
</script>
</body>
</html>
This example shows how encapsulation can be used to protect the internal state of a BankAccount
object. The #balance
property is private, so it cannot be modified directly from outside the class. Instead, the deposit
and withdraw
methods provide controlled access to modify the balance, and the getBalance
method allows you to check the balance.
Challenge
Now that you've seen how encapsulation works in a BankAccount
class, try extending this example with the following challenge:
- Add a method to the
BankAccount
class that allows setting an account holder's name. Use a setter to ensure the name is not empty. - Modify the
getBalance
method to return the account holder's name along with the balance.
In order to check your learning, you should attempt to create a solution before revealing the provided solution below.
// JavaScript Code for the Challenge Solution
class BankAccount {
#balance = 0; // Private field
#accountHolderName; // Private field
deposit(amount) {
if (amount > 0) {
this.#balance += amount;
console.log(`Deposited: $${amount}`);
}
}
withdraw(amount) {
if (amount > 0 && amount <= this.#balance) {
this.#balance -= amount;
console.log(`Withdrew: $${amount}`);
} else {
console.log('Insufficient funds or invalid amount');
}
}
set accountHolderName(name) {
if (name.length > 0) {
this.#accountHolderName = name;
} else {
console.log('Account holder name cannot be empty');
}
}
getBalance() {
return `${this.#accountHolderName}'s balance: $${this.#balance}`;
}
}
const account = new BankAccount();
account.accountHolderName = 'John Doe';
account.deposit(100);
account.withdraw(50);
console.log(account.getBalance()); // Outputs: John Doe's balance: $50