Encapsulation and Private Fields
Objectives
By the end of this chapter, you should be able to:
- Explain encapsulation: keeping an object’s internal details separate from what it exposes
- Identify the problem that happens when internal state is left fully public
- Declare private fields with
#fieldName - Prove that private fields can’t be accessed from outside the class
💡 Why this matters: A class is a promise about how its objects can be used. If any piece of code, anywhere, can directly rewrite an object’s internal data, that promise is worthless. Encapsulation is how a class protects itself from being used incorrectly.
The Problem: Fully Public State
Here’s a class where nothing is protected. Anyone with the object can do anything to it.
class LeakyAccount {
constructor(owner, startingBalance) {
this.owner = owner;
this.balance = startingBalance;
}
}
const account = new LeakyAccount("Priya", 100);
console.log(account.balance);
// 100
account.balance = -1000000;
console.log(account.balance);
// -1000000
Nothing stopped that. balance is a plain public property, so any code that touches account can set it to anything, including a nonsense negative number, with no deposit or withdrawal ever happening. A bank account class should never allow that.
Encapsulation: Hiding the Details
Encapsulation means keeping an object’s internal data hidden, and only allowing it to change through methods the class controls. The object exposes a small, deliberate interface (deposit, withdraw, getBalance), and hides the raw data behind it. Think of a microwave: you press buttons on the outside, you never touch the wiring inside.
Private Fields with #
JavaScript enforces this with private fields, written with a # prefix. A private field can only be read or written from inside the class it’s declared in.
class BankAccount {
#balance;
constructor(owner, startingBalance) {
this.owner = owner;
this.#balance = startingBalance;
}
deposit(amount) {
this.#balance += amount;
return this.#balance;
}
withdraw(amount) {
if (amount > this.#balance) {
return "Insufficient funds";
}
this.#balance -= amount;
return this.#balance;
}
getBalance() {
return this.#balance;
}
}
const account = new BankAccount("Priya", 100);
console.log(account.getBalance());
// 100
console.log(account.deposit(50));
// 150
console.log(account.withdraw(30));
// 120
console.log(account.owner);
// Priya
#balance is declared at the top of the class and only ever touched inside deposit, withdraw, getBalance, and the constructor. owner stays a normal public property, so it’s still readable from outside, but #balance isn’t.
Proving It’s Actually Private
This isn’t a naming convention or a suggestion, it’s enforced by JavaScript itself. Trying to read #balance from outside the class doesn’t just return undefined, it refuses to run at all:
class BankAccount {
#balance;
constructor(owner, startingBalance) {
this.owner = owner;
this.#balance = startingBalance;
}
}
const account = new BankAccount("Priya", 100);
console.log(account.#balance);
// SyntaxError: Private field '#balance' must be declared in an enclosing class
JavaScript won’t even let you write account.#balance outside a class that declares #balance. Note also that a plain, non-private property named balance doesn’t exist on this object at all:
console.log(account.balance);
// undefined
The only way to read the balance is through getBalance(), and the only way to change it is through deposit() and withdraw(), both of which enforce rules like refusing an overdraft. That’s encapsulation in practice.
Try It
- Write a class
Thermostatwith a private field#temperature, a constructor that sets a starting temperature, asetTemperature(value)method that only accepts values between10and30(otherwise return"Out of range"), and agetTemperature()method. - Confirm from outside the class that
myThermostat.#temperaturefails, and thatmyThermostat.temperatureisundefined. - Add a
#historyprivate array field toThermostatthat records every value passed tosetTemperature, plus a public methodgetHistory()that returns a copy of it.
Recap
- Encapsulation means hiding an object’s internal data and only changing it through methods the class defines.
- Without it, any code can put an object into an invalid state, like a negative bank balance.
#fieldNamedeclares a private field, only reachable from inside the class.- Accessing a private field from outside the class throws a real error, it isn’t just convention.
Next lesson: inheritance with extends and super(), and polymorphism, where the same method call behaves differently depending on the object’s actual class.