Lesson 60 of 80
The underscore-prefix convention (_balance) was always just a hint — anyone could still touch it. The # syntax is real, enforced privacy, checked by the engine, not by politeness.
12 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
Before this feature, "private" in a JS class meant one thing: naming a property _balance and trusting everyone to leave it alone. Nothing stopped account._balance = 999999 from working exactly like touching any other property — the underscore was a social contract, not a barrier.
Fields and methods prefixed with # are different in kind, not just in naming style. A #balance field genuinely doesn't exist from outside the class — not "exists but you shouldn't touch it," but syntactically inaccessible. account.#balance outside the class isn't a runtime access-denied error you catch; it's a SyntaxError at parse time, because #balance isn't even valid syntax unless you're inside a class that declared it.
class BankAccount {
#balance;
constructor(openingBalance) {
this.#balance = openingBalance;
}
deposit(amount) {
this.#assertPositive(amount);
this.#balance += amount;
return this.#balance;
}
withdraw(amount) {
this.#assertPositive(amount);
if (amount > this.#balance) throw new Error("Insufficient funds");
this.#balance -= amount;
return this.#balance;
}
get balance() {
return this.#balance; // read-only view from outside
}
#assertPositive(amount) {
if (amount <= 0) throw new Error("Amount must be positive");
}
}
const acct = new BankAccount(100);
acct.deposit(50);
acct.balance; // 150 — fine, going through the public getter
acct.#balance; // SyntaxError — not even parseable outside the class
acct.#balance = 999999; // same SyntaxError — there's no way around it, not even with tricks that defeat _underscore convention
#assertPositive is a private method — same rule, just for behavior instead of data. It's only callable from inside the class, which means it can be a genuinely internal helper without cluttering the public API or risking someone calling it directly with bad assumptions.
#balance — private fields are scoped to the exact class body that declares them, not the whole prototype chain. A subclass needs the parent to expose access through a protected-feeling but still public method or getter.this[fieldName] works for public properties with any string key; #fieldName must be written literally in the source as a fixed identifier — there's no this[#dynamicName].this.#undeclaredField where #undeclaredField was never declared in the class is a SyntaxError, not undefined.Before # fields existed, the only way to get actual enforced privacy (not just convention) was the module pattern — hiding state in a closure and returning only the functions meant to be public. # fields bring that same real privacy guarantee to class syntax directly, without needing to route object construction through a closure-returning factory function to get it.