CodingNic

Object-Oriented Programming

Encapsulation and Private Fields

Object-Oriented Programming 20 min read

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.

javascript
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.

javascript
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:

javascript
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:

javascript
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

  1. Write a class Thermostat with a private field #temperature, a constructor that sets a starting temperature, a setTemperature(value) method that only accepts values between 10 and 30 (otherwise return "Out of range"), and a getTemperature() method.
  2. Confirm from outside the class that myThermostat.#temperature fails, and that myThermostat.temperature is undefined.
  3. Add a #history private array field to Thermostat that records every value passed to setTemperature, plus a public method getHistory() 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.
  • #fieldName declares 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.