Learn by reading through in order

Private Fields (#) and Encapsulation

Private fields and methods use a leading # to block reading and writing from outside the class. Covers the SyntaxError raised before code runs and how # differs from the _ convention.

Storing a prepaid card's balance in _balance doesn't protect it: card._balance = 999999 anywhere in the page's code sets that value directly. A leading _ is only a convention meaning "don't touch this from outside"; it doesn't stop assignments or calls.

This article covers private fields and private methods, which can't be read or written from outside the class.

Hiding the Balance Inside the Class — Private Fields

Say you want the balance to change only through charge, which enforces a limit of 20,000 yen per top-up. With _balance, if the code for a refund screen writes card._balance += 30000, nothing stops it, and an amount over the limit goes into the balance.

You declare a private field (a field in a class whose name starts with #, which can be read and written only inside that class body) as #balance = 0; and use it as this.#balance.

The # is part of the name, so this.balance and this.#balance are different names.

class PrepaidCard {
  #balance = 0;                              // Balance. Can't be read or written outside the class

  charge(amount) {
    if (amount > 20000) {
      throw new Error(`Each top-up is limited to 20000 yen: ${amount}`);
    }
    this.#balance += amount;                 // Inside the class, so it can be changed
  }

  get balance() { return this.#balance; }    // Expose only a getter for reading
}

const card = new PrepaidCard();
card.charge(5000);                           // charge is the only way to change the balance
card.charge(3000);
console.log(card.balance);                   // 8000
Lines That Can and Can't Reach #balance
Body of class PrepaidCard
  • #balance = 0 — a name usable only inside this body
Body of charge(amount)
  • If amount > 20000, throw runs
  • Only amounts that pass do this.#balance += amount
Body of get balance()
  • return this.#balance — only returns the value, never changes it
Lines outside the class
  • card.charge(5000) — changes the balance through charge
  • card.balance — reads 8000 through the getter
  • The name #balance can't be written here
You can write #balance only inside the class body. For code outside, charge is the only way to change the balance.

Thanks to encapsulation (a design that bundles data and the code that changes it into a class, and limits outside use to defined entry points), the only way to change the balance of this PrepaidCard is charge, and the only way to read it is the balance getter.

Using an Undeclared # Name Is a SyntaxError

Declare a # name in the class body, as #balance = 0; or #balance;, before you use it. If you assign this.#balance = 0; in the constructor without declaring it, you get a SyntaxError, even though the line is inside the class. Assigning to a # name doesn't create it.

You make sure a member's point balance can change only through purchases and redemptions. PointAccount and orders are already declared.

① Change the balance to a field that can't be read or written from outside the class.

② Add points, which only reads the balance.

③ Process the orders in order, displaying the balance each time.

④ Assign 999 to the old name, _points, and display "Balance: X pt".

(If it runs correctly, an explanation will appear.)

JavaScript / TypeScript Editor

Run code to see output

Writing It from Outside Fails Before Anything Runs — SyntaxError

Suppose you want to set the balance to 999999 from the page's code to test something. If you write card.#balance = 999999, as you would have with _balance, that line fails. Worse, even the lines above it, which should have printed first, print nothing.

Writing card.#balance outside the class raises SyntaxError: Private field '#balance' must be declared in an enclosing class while the code is being loaded, just like redeclaring a let. It means "a private field must be declared in the class that encloses it".

class PrepaidCard {
  #balance = 0;

  charge(amount) { this.#balance += amount; }
  get balance() { return this.#balance; }
}

console.log("Starting the test");   // This line isn't printed either

const card = new PrepaidCard();
card.charge(5000);

// With _balance, lines run from the top, but a # name stops the code while it's being loaded
card.#balance = 999999;             // SyntaxError: Private field '#balance' must be declared in an enclosing class
console.log(card.balance);
How Far an Assignment from Outside Gets
card._balance= 999999Runs fromthe first line"Starting thetest" is printedAssignment works:balance 999999card.#balance= 999999Syntax is checkedwhile loadingA # name appearsoutside the classError is foundbefore running
The top row gets as far as the assignment, and the earlier output still appears. The bottom row stops before running the first line.

If you get a SyntaxError starting with Private field and nothing is printed at all, look for a # name written outside the class. Rewrite any line that changes the value from outside so that it calls a class method, such as charge.

The store's page added to a visit coupon's remaining uses by writing to the # name directly. Change that into calls to a class method. VisitCoupon and coupon are already declared.

① Use the coupon twice and display "Uses left: X".

② Add addUses to the class so that the remaining uses never go above 5.

③ Add 2 uses and display the remaining uses.

④ Add 4 more uses and display the remaining uses.

JavaScript / TypeScript Editor

Run code to see output

Keeping Internal Steps Out of Reach — Private Methods

Say you move the code that lowers the balance out of pay into a regular method called withdraw. Now code outside the class can also call card.withdraw(5000), skip the balance check in pay, and push the balance below zero.

If you make it a private method (a method defined with a leading #, which can be called only from methods of the same class, in the form this.#withdraw(price)), the step you moved out can't be called from outside. If you later change its name or parameters, the changes stay inside the class.

class PrepaidCard {
  #balance = 3000;

  // A step that only lowers the balance. pay checks first whether there's enough
  #withdraw(amount) { this.#balance -= amount; }

  pay(price) {
    if (price > this.#balance) {
      return `Insufficient balance: ${price}`;
    }
    this.#withdraw(price);                     // Inside the class, so it can be called
    return `Balance after payment: ${this.#balance}`;
  }
}

const card = new PrepaidCard();
console.log(card.pay(480));                    // Balance after payment: 2520
console.log(card.pay(5000));                   // Insufficient balance: 5000
console.log(typeof card.withdraw);             // undefined (calling it throws a TypeError)
Which Calls Get as Far as #withdraw
#withdraw(amount)lowers the balancecard.pay(480)card.pay(5000)card.withdraw(5000)480 is withinthe 3000 balance5000 is more thanthe 2520 balanceNo method namedwithdraw (no #)Goes through#withdraw: 2520Never reaches#withdraw: 2520Throws aTypeError
Only the left column, which passes the check, reaches the step that lowers the balance. The name withdraw, without the #, doesn't exist on the instance.

pay(5000) returns at the balance check, so it never gets as far as #withdraw. Writing card.#withdraw(5000) outside the class is a SyntaxError while the code is being loaded, just like #balance; the TypeError in the right column comes from calling a different name, one without the #.

Call Private Methods with this.

Even inside the class, if you leave out this. and write #withdraw(price);, you get SyntaxError: Unexpected identifier '#withdraw' before anything runs. Just like private fields, private methods are always accessed as this.#withdraw(price).

You calculate a bike rental's current fee and the extra charge for an extension using the same formula. BikeRental and rides are already declared.

① Add calcFee, which calculates the fee, as a method that can't be called from outside.

② Using ①, add a fee property that returns the current fee and an extraFee method that returns the extra charge for an extension.

③ Display "X min: X yen (+X yen for 10 more min)" for every ride.

④ Read calcFee on the first ride from outside and display its type.

JavaScript / TypeScript Editor

Run code to see output

What Ends Up in JSON — _ vs. #

Suppose you switch a prepaid card class from _balance to #balance. That blocks assignments from outside, but if you were saving instances as strings with JSON.stringify, the balance disappears from the data saved after the switch.

A name with a leading _ is an ordinary property that just happens to start with _, so it shows up in JSON.stringify. # fields aren't property keys, so they're left out of the result. A getter is defined on the class, not on the instance, so it isn't a key either, and balance doesn't appear.

// Before the switch: an ordinary property with a leading _
class LegacyPrepaidCard {
  constructor() { this._balance = 3000; }
}

// After the switch: a private field with # and a getter for reading it
class PrepaidCard {
  #balance = 3000;
  get balance() { return this.#balance; }
}

const legacyCard = new LegacyPrepaidCard();
const prepaidCard = new PrepaidCard();

console.log(JSON.stringify(legacyCard));                         // {"_balance":3000}
console.log(JSON.stringify(prepaidCard));                        // {}
console.log(JSON.stringify({ balance: prepaidCard.balance }));   // {"balance":3000}
What JSON.stringify Reads
prepaidCard (an instance with #balance)
Property keys
  • No keys at all (get balance() is a method written on the class)
  • JSON.stringify(prepaidCard) gives {}
Private fields
  • #balance — 3000
  • Only lines inside PrepaidCard can read it
#balance belongs to the instance but isn't a property key. JSON.stringify reads only property keys.

Even though you get {}, the balance hasn't disappeared: prepaidCard.balance still reads 3000. When saving, pass an object built from the getter's value, as in the last line of the sample. The table below compares the three approaches used in this article.

ApproachAccess from OutsideJSON.stringify Result
Name with _ (_balance)Readable and writable, no checksIncluded, as in {"_balance":3000}
Private field (#balance)SyntaxError while loadingLeft out ({})
Private method (#withdraw)#withdraw(): SyntaxError / withdraw(): TypeErrorLeft out, since it's a method
QUIZ

Knowledge Check

Answer each question one by one.

Q1What happens when you run code that has card.#balance = 0; outside the class, after a console.log?

Q2On a card that has #withdraw, what happens if you call card.withdraw(500) from outside?

Q3What does JSON.stringify give for a card with #balance = 3000 and only a getter?