JavaScript Encapsulation
Table of Contents + −
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:
class BankAccount { #balance = 0;
deposit(amount) { this.#balance += amount; }
getBalance() { return this.#balance; }}
const account = new BankAccount();account.deposit(100);console.log(account.getBalance()); // 100Let’s walk through what each line does:
#balance = 0declares a private field. The#prefix makes it private, so it can only be used inside this class and starts at0.this.#balance += amountruns insidedepositand adds the passed amount to the private field.return this.#balanceinsidegetBalancehands back the current value, since outside code can’t read#balanceon its own.account.deposit(100)raises the balance to100, andgetBalance()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:
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()); // 70Let’s trace how the rules guard the private #balance:
if (amount <= 0) returnindepositrejects zero or negative amounts before touching#balance, so only valid deposits go through.if (amount > this.#balance)inwithdrawchecks the private balance first and stops the withdrawal if there isn’t enough money.this.#balance -= amountonly runs after that check passes, so the balance can never drop below zero.- The script deposits
100, withdraws30, andgetBalance()returns70because 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:
// ❌ A public field lets anyone set anythingaccount.balance = -5000;account.balance = "lots of money";Here is what these lines would do if balance were public:
account.balance = -5000reaches straight into the object and sets a negative balance, skipping every rule inwithdraw.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!
- Create a
BankAccountclass with a private#balancefield set to0. - Add a
deposit(amount)method that ignores amounts that are zero or negative. - Add a
withdraw(amount)method that refuses to withdraw more than the balance. - Add a
getBalance()method, then try to readaccount.#balancedirectly 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, andgetBalance - ✅ 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.