Encapsulation is one of the fundamental concepts of Object-Oriented Programming (OOP). It means grouping data and the methods that operate on that data into a single unit, such as a class, while controlling how the internal data can be accessed or modified.
In JavaScript, encapsulation helps protect an object’s internal state from unintended changes. You can use private class fields, methods, getters, and setters to control how other parts of your program interact with an object.
For example, a bank account should not allow anyone to change its balance to an invalid value directly. Encapsulation allows you to define rules for depositing and withdrawing money.
In this tutorial, you will learn what encapsulation is, why it is important, and how to implement it using JavaScript classes.
What Is Encapsulation in JavaScript?
Encapsulation is the practice of combining related data and behavior within an object and controlling access to its internal state.
Consider a bank account with a balance. If the balance can be changed without any restrictions, a program might accidentally assign a negative value or bypass important validation.
Encapsulation lets you keep the balance private and provide methods that validate changes.
Example Without Encapsulation
class BankAccount {
constructor(balance) {
this.balance = balance;
}
}
const account = new BankAccount(1000);
account.balance = -500;
console.log(account.balance);
Output:
-500
The balance can be changed directly, even though a negative balance might be invalid for this account.
Example With Encapsulation
class BankAccount {
#balance;
constructor(balance) {
this.#balance = balance;
}
deposit(amount) {
if (amount <= 0) {
throw new Error("Deposit must be greater than zero");
}
this.#balance += amount;
}
getBalance() {
return this.#balance;
}
}
const account = new BankAccount(1000);
account.deposit(500);
console.log(account.getBalance());
Output:
1500
In this example, #balance is a private field. It cannot be accessed directly from outside the class. The deposit() method controls how the balance changes, and getBalance() provides read access.
This helps maintain the object’s internal state.
Why Is Encapsulation Important?
Encapsulation makes JavaScript applications easier to maintain and less vulnerable to accidental data changes.
Its main benefits include:
- Data protection: Private fields prevent direct access from outside the class.
- Controlled modification: Methods can validate values before changing internal state.
- Better organization: Data and related behavior stay together.
- Maintainability: Internal implementation can change without requiring every caller to change.
- Consistency: The object can enforce rules that keep its state valid.
Encapsulation does not automatically make an application secure. Sensitive data and security rules must also be protected at the appropriate system boundaries, especially on servers.
Private Fields in JavaScript
JavaScript supports private class fields using the # prefix. A private field can only be accessed from the class body where it is declared, including permitted methods and accessors.
Example of a Private Field
class User {
#password;
constructor(password) {
this.#password = password;
}
checkPassword(password) {
return this.#password === password;
}
}
const user = new User("secret123");
console.log(user.checkPassword("secret123"));
console.log(user.checkPassword("wrong123"));
Output:
true
false
The #password field stores the password value privately. The checkPassword() method provides a controlled way to compare a supplied value with it.
For a real application, passwords should not be stored as plain text. Authentication systems should use appropriate password hashing and secure verification.
Accessing a Private Field Directly
class User {
#name = "Karim";
}
const user = new User();
console.log(user.#name);
Result: This code produces a syntax error because #name is private to the class body.
You cannot bypass this restriction by using ordinary bracket notation, such as user["#name"].
Using Methods to Control Data
Methods allow you to define rules for accessing or changing private data.
Example of Data Validation
class Product {
#price;
constructor(price) {
this.#price = price;
}
setPrice(price) {
if (price < 0) {
throw new Error("Price cannot be negative");
}
this.#price = price;
}
getPrice() {
return this.#price;
}
}
const product = new Product(1000);
product.setPrice(1500);
console.log(product.getPrice());
Output:
1500
If you try to set a negative price, the method throws an error instead of updating the private field.
product.setPrice(-500);
Output: An error is thrown with the message Price cannot be negative.
The important point is that validation happens inside the class, where the internal data is managed.
Getters and Setters in Encapsulation
JavaScript provides getters and setters to control how properties are read and modified.
- A getter allows a property to be read through a method-like accessor.
- A setter allows a property to be assigned through a method-like accessor.
These accessors are useful when you want a natural property interface while keeping the underlying data private.
Example of a Getter and Setter
class Employee {
#salary;
constructor(salary) {
this.#salary = salary;
}
get salary() {
return this.#salary;
}
set salary(value) {
if (value < 0) {
throw new Error("Salary cannot be negative");
}
this.#salary = value;
}
}
const employee = new Employee(30000);
console.log(employee.salary);
employee.salary = 35000;
console.log(employee.salary);
Output:
30000
35000
Notice that the getter and setter are used like a property:
employee.salary;
employee.salary = 35000;
You do not need to call them as employee.salary() or employee.salary(35000).
When Should You Use Getters and Setters?
Use getters and setters when reading or updating a property needs validation, calculated values, or other controlled behavior.
However, not every property needs a getter or setter. For simple data that does not require restrictions, ordinary public properties may be sufficient.
Complete Example of Encapsulation in JavaScript
The following example demonstrates a bank account with private data, validation, and controlled operations.
class BankAccount {
#owner;
#balance;
constructor(owner, balance) {
if (balance < 0) {
throw new Error("Initial balance cannot be negative");
}
this.#owner = owner;
this.#balance = balance;
}
get owner() {
return this.#owner;
}
get balance() {
return this.#balance;
}
deposit(amount) {
if (amount <= 0) {
throw new Error("Deposit must be greater than zero");
}
this.#balance += amount;
}
withdraw(amount) {
if (amount <= 0) {
throw new Error("Withdrawal must be greater than zero");
}
if (amount > this.#balance) {
throw new Error("Insufficient balance");
}
this.#balance -= amount;
}
}
const account = new BankAccount("Karim", 2000);
account.deposit(500);
account.withdraw(300);
console.log(account.owner);
console.log(account.balance);
Output:
Karim
2200
How This Example Works
#ownerand#balanceare private fields.- The constructor validates the initial balance.
- The getters provide read access to the owner’s name and balance.
deposit()accepts only positive deposit amounts.withdraw()validates the amount and checks the available balance.- External code cannot directly assign a new value to
#balance.
The class keeps the account’s internal state under its own control.
Encapsulation vs Abstraction
Encapsulation and abstraction are related OOP concepts, but they have different purposes.
| Encapsulation | Abstraction |
|---|---|
| Groups data and behavior and controls access to internal state | Hides unnecessary implementation details and exposes essential functionality |
| Focuses on controlling how data is accessed or modified | Focuses on what an object provides without requiring users to understand how it works |
| Uses private fields, methods, getters, and setters | Can use public methods, interfaces, and well-designed APIs |
| Example: protecting a bank account balance | Example: using withdraw() without knowing the internal balance-update logic |
Encapsulation helps implement abstraction by hiding internal details, but the two concepts are not identical.
Common Mistakes When Using Encapsulation
Using Public Properties for Data That Requires Protection
A public property can be changed directly from outside the class. Use a private field when the class needs to control access to its internal state.
Forgetting the # Prefix
Private fields must be declared and referenced using the # prefix.
class User {
#name = "Karim";
getName() {
return this.#name;
}
}
Writing this.name instead of this.#name refers to a different, ordinary property.
Adding Getters and Setters Without a Purpose
Getters and setters are not required for every property. Use them when they provide meaningful validation, calculated values, or control over access.
Assuming Encapsulation Automatically Validates Data
A private field protects access, but it does not automatically validate its value. The constructor and methods must implement the rules your application needs.
Confusing Private Fields with Underscore Naming
A property such as _balance is still publicly accessible in ordinary JavaScript. The underscore is only a naming convention.
By contrast, #balance is a genuine private class field enforced by the language.
FAQs About JavaScript Encapsulation
What is encapsulation in JavaScript?
Encapsulation is an OOP concept that combines related data and behavior in an object and controls how its internal state is accessed or changed.
How do you achieve encapsulation in JavaScript?
You can use classes, private fields beginning with #, methods, getters, and setters to control access to an object’s data.
What is a private field in JavaScript?
A private field is a class field declared with #. It can only be accessed from within the class body where it is declared.
What is the difference between a public property and a private field?
A public property can generally be accessed and modified from outside the object. A private field is restricted to its declaring class body.
Are getters and setters necessary for encapsulation?
No. They are useful when you need controlled property access, validation, or calculated values. Private fields and ordinary methods can also provide encapsulation.
What is an example of encapsulation in real life?
A bank account is a common example. The account balance is kept internal, while deposit and withdrawal operations control how it changes.
What are the advantages of encapsulation?
Encapsulation helps protect internal state from accidental changes, improves code organization, supports validation, and makes applications easier to maintain.
Conclusion
Encapsulation is an important JavaScript OOP concept that combines data and behavior while controlling access to an object’s internal state.
Private fields help prevent direct external access, methods enforce rules, and getters and setters provide controlled ways to read or update properties.
By applying encapsulation to classes such as bank accounts, users, and products, you can build JavaScript applications that are more organized, maintainable, and consistent.