What are private class fields in JavaScript?
Assesses fundamental understanding of JavaScript conventions, runtime behavior, and memory/performance considerations.
Hiring managers look for precision, avoidance of ambiguous jargon, and ability to explain trade-offs under real production conditions.
Private class fields (introduced in ES2022) are class properties that are only accessible within the class itself. They are prefixed with a hash symbol (#) and provide true encapsulation in JavaScript classes.
Key Features:
- True Privacy: Cannot be accessed from outside the class, even using bracket notation
- Instance Privacy: Each instance has its own private fields
- Subclass Isolation: Private fields are not inherited or accessible by subclasses
- Hard Private: Unlike convention-based privacy (e.g.,
_privateField), these are enforced by the language
Syntax and Examples:
class BankAccount {
// Private fields (must be declared at class level)
#balance = 0;
#accountNumber;
#pin;
// Public field
accountHolder;
constructor(holder, accountNumber, initialDeposit, pin) {
this.accountHolder = holder;
this.#accountNumber = accountNumber;
this.#balance = initialDeposit;
this.#pin = pin;
}
// Private method
#validatePin(inputPin) {
return this.#pin === inputPin;
}
// Public methods can access private fields
deposit(amount) {
if (amount > 0) {
this.#balance += amount;
return true;
}
return false;
}
withdraw(amount, pin) {
if (!this.#validatePin(pin)) {
throw new Error('Invalid PIN');
}
if (amount > 0 && amount <= this.#balance) {
this.#balance -= amount;
return amount;
}
throw new Error('Insufficient funds');
}
getBalance(pin) {
if (!this.#validatePin(pin)) {
throw new Error('Invalid PIN');
}
return this.#balance;
}
// Static private fields
static #bankName = 'SecureBank';
static getBankName() {
return this.#bankName;
}
}
// Usage
const account = new BankAccount('Alice', '123456', 1000, '1234');
account.deposit(500);
console.log(account.getBalance('1234')); // 1500
// Attempting to access private fields throws an error
console.log(account.#balance); // SyntaxError: Private field '#balance' must be declared in an enclosing class
console.log(account['#balance']); // undefined (bracket notation doesn't work)
// Even reflection doesn't work
console.log(Object.keys(account)); // ['accountHolder']
console.log(Reflect.ownKeys(account)); // Does not include private fields in public APIs
Benefits over Convention-Based Privacy:
// Old way (convention-based, not truly private)
class OldAccount {
constructor(balance) {
this._balance = balance; // Convention: underscore means "private"
}
getBalance() {
return this._balance;
}
}
const oldAcc = new OldAccount(1000);
console.log(oldAcc._balance); // 1000 (accessible! Not truly private)
oldAcc._balance = 999999; // Can be modified from outside
// New way (truly private)
class NewAccount {
#balance;
constructor(balance) {
this.#balance = balance;
}
getBalance() {
return this.#balance;
}
}
const newAcc = new NewAccount(1000);
// console.log(newAcc.#balance); // SyntaxError
// newAcc.#balance = 999999; // SyntaxError
Private Fields with Inheritance:
class Parent {
#privateField = 'parent private';
getPrivate() {
return this.#privateField;
}
}
class Child extends Parent {
#privateField = 'child private'; // Different field, doesn't override
getChildPrivate() {
return this.#privateField;
}
}
const child = new Child();
console.log(child.getPrivate()); // 'parent private'
console.log(child.getChildPrivate()); // 'child private'
Candidate Response Strategy & Interview Tips
- Start with a concise one-sentence summary: Deliver a direct, confident answer first before expanding into nuances.
- Demonstrate real-world trade-offs: Discuss where this approach excels and when you would avoid it in production systems.
- Discuss complexity & edge cases: Proactively explain time/space complexity or boundary conditions (null values, scale limits).
- Prepare for interviewer follow-ups: Technical hiring panels frequently probe deeper into concurrency, backward compatibility, or alternative libraries.