Private Methods & Accessors
Master true encapsulation in JavaScript. Learn how to use the `#` prefix to create private class members that are physically inaccessible from outside the class instance, ensuring robust API boundaries.
Hard Encapsulation with #
For decades, JavaScript developers used the "underscore convention" (e.g., `_privateVar`) to signal that a property shouldn't be touched. However, this was only a social contract—the property was still fully accessible. **Class Private Fields** introduce "Hard Encapsulation" at the engine level. Once a field is prefixed with `#`, attempting to access it from outside the class scope results in a **SyntaxError**, not just `undefined`.
// Engineering Pattern: Hard Encapsulation
class SecureVault {
// Private Field Declaration
#masterKey;
#balance = 0;
constructor(initialKey) {
this.#masterKey = initialKey;
}
// Private Method for Internal Logic
#logTransaction(type, amount) {
console.log(`Vault event: ${type} of $${amount}`);
}
deposit(amount, key) {
if (key !== this.#masterKey) throw new Error("Unauthorized");
this.#balance += amount;
this.#logTransaction("DEPOSIT", amount);
}
get balance() {
return this.#balance; // Read-only view
}
}
const vault = new SecureVault("SECRET_123");
vault.deposit(100, "SECRET_123");
// console.log(vault.#balance); // SyntaxError: Private field '#balance' must be declared in an enclosing classStatic Private & Initialization
Privacy extends to class-level members as well. **Static Private Fields** are useful for managing shared internal state, such as singleton instances or connection pools, without exposing them to global modification. Combined with **Static Initialization Blocks**, you can perform complex, one-time setup logic for these private members.
// Technical Insight: Static Private Members
class SingletonService {
static #instance;
static {
// Static initialization block
this.#instance = new SingletonService();
}
static getInstance() {
return this.#instance;
}
#internalState = "init";
// Private internal method
#refresh() { /* ... */ }
}Technical Insight: The "Why" of #
Why did TC39 choose the `#` symbol? Unlike `private` in TypeScript (which is purely compile-time), JavaScript needed a way to distinguish private properties from public ones at **runtime** without breaking backward compatibility or complicating property lookup performance. The `#` prefix ensures that the Engine knows immediately if a property access is valid or should throw a error, maintaining O(1) look-up performance.
Private Member Checklist:
- ✅ **Stability:** Use `#` for internal state that consumers should never rely on.
- ✅ **Security:** Hide sensitive data like keys or internal IDs from `JSON.stringify()`.
- ✅ **Cleanliness:** Use private methods to break down complex public functions into manageable units.
- ✅ **Inheritance:** Remember that private fields are NOT visible to subclasses (use composition if needed).
- ✅ **Evolution:** Move from underscore conventions (`_prop`) to true `#` fields for modern libraries.