JavaScript Encapsulation

In the previous lesson, we learned how one class can reuse another through inheritance. Now let’s learn how to protect the data inside a class so the outside world can’t change it directly.

🔒 What is Encapsulation?

Encapsulation means two things working together. First, you bundle the data and the methods that use it inside one class. Second, you hide the internal details so they can’t be changed directly from outside. The outside code talks to the object only through the methods you allow.

Think of a class as a sealed box. The data lives inside, and the methods are the buttons on the outside. You press a button to ask the object to do something, but you never reach into the box and grab the data yourself.

Term Meaning
Bundling Keeping data and the methods that use it in one place
Hiding Stopping outside code from reading or changing the data directly
Controlled access Letting outside code change data only through approved methods

🏦 Private Fields with #

A private field is a piece of data that lives inside the class and cannot be read or changed from outside it. In JavaScript, you make a field private by starting its name with a #.

Here is a BankAccount class with a private #balance:

encapsulation.js
class BankAccount {
#balance = 0;
deposit(amount) {
this.#balance += amount;
}
getBalance() {
return this.#balance;
}
}
const account = new BankAccount();
account.deposit(100);
console.log(account.getBalance()); // 100

Let’s walk through what each line does:

  • #balance = 0 declares a private field. The # prefix makes it private, so it can only be used inside this class and starts at 0.
  • this.#balance += amount runs inside deposit and adds the passed amount to the private field.
  • return this.#balance inside getBalance hands back the current value, since outside code can’t read #balance on its own.
  • account.deposit(100) raises the balance to 100, and getBalance() reads it back, because methods are the only way in.

The # is part of the name

The # is not an operator. It is part of the field’s name, so it must always be written as this.#balance, never as this.balance.

🚪 Exposing Controlled Access

Hiding the data is only half the job. You still need a way for outside code to use it. You do that with public methods that let people interact with the data on your terms.

This version of BankAccount adds a withdraw method that refuses to let the balance go negative:

encapsulation.js
class BankAccount {
#balance = 0;
deposit(amount) {
if (amount <= 0) return; // ❌ ignore invalid amounts
this.#balance += amount;
}
withdraw(amount) {
if (amount > this.#balance) {
console.log("Not enough money");
return;
}
this.#balance -= amount;
}
getBalance() {
return this.#balance;
}
}
const account = new BankAccount();
account.deposit(100);
account.withdraw(30);
console.log(account.getBalance()); // 70

Let’s trace how the rules guard the private #balance:

  • if (amount <= 0) return in deposit rejects zero or negative amounts before touching #balance, so only valid deposits go through.
  • if (amount > this.#balance) in withdraw checks the private balance first and stops the withdrawal if there isn’t enough money.
  • this.#balance -= amount only runs after that check passes, so the balance can never drop below zero.
  • The script deposits 100, withdraws 30, and getBalance() returns 70 because every change passed through these guarded methods.

Because the balance is private, the rules inside deposit and withdraw always run. There is no way to skip them and set the balance to a bad value, so the account can never end up in a broken state.

🛡️ Why Hiding State Prevents Bugs

When data is public, any line of code anywhere can change it. That makes bugs hard to find, because the bad value could have come from anywhere.

With a public balance, code like this would quietly break your account:

encapsulation.js
// ❌ A public field lets anyone set anything
account.balance = -5000;
account.balance = "lots of money";

Here is what these lines would do if balance were public:

  • account.balance = -5000 reaches straight into the object and sets a negative balance, skipping every rule in withdraw.
  • account.balance = "lots of money" replaces the number with text, leaving the account in a state no method ever expected.

A private field blocks both of these. The balance can only change through deposit and withdraw, and those methods check every change. When something goes wrong, you have only a few methods to look at instead of the whole program.

Validate inside the methods

Put your rules, like “amount must be positive,” inside the methods that change a private field. That way every change is checked in one place.

⚠️ Common Mistakes to Avoid

Mistake Problem Solution
Reading a # field from outside account.#balance throws a SyntaxError Add a method like getBalance() to read it
Hiding a field but giving no way to read it The value becomes useless because nothing can see it Expose a method or getter that returns it
Overexposing internals A setBalance() method makes hiding pointless Only expose actions like deposit and withdraw

🔧 Try It Yourself!

  1. Create a BankAccount class with a private #balance field set to 0.
  2. Add a deposit(amount) method that ignores amounts that are zero or negative.
  3. Add a withdraw(amount) method that refuses to withdraw more than the balance.
  4. Add a getBalance() method, then try to read account.#balance directly and watch it fail.

🧩 What You’ve Learned

  • ✅ Encapsulation bundles data with its methods and hides the internal details
  • ✅ A field name starting with # is private and cannot be touched from outside
  • ✅ You expose data through controlled methods like deposit, withdraw, and getBalance
  • ✅ Putting rules inside those methods keeps the data valid and prevents bugs
  • ✅ A class without a way to read its hidden data is useless, so always expose one

🚀 What’s Next?

Now that you can hide data inside a class, we will learn another way to keep data private using functions. Let’s continue to Closures.

Share & Connect