The Final Frontier: Private Fields
Before ES2022, JavaScript lacked true encapsulation, relying on naming conventions like total _underscores. With Private Fields, JS now provides language-level "Hard Privacy" that cannot be bypassed by external code.
Hard Privacy with the Hash (#) Syntax
Private fields, prefixed with #, are not merely hidden; they are unreachable from outside the class body. This ensures that internal implementation details can change without breaking consumer code—the fundamental promise of Object-Oriented Programming.
// --- ES2022 Hard Privacy (#) ---
class SecureVault {
// 1. Declaration is mandatory
#secretKey;
#accessCount = 0;
constructor(initialKey) {
this.#secretKey = initialKey;
}
// 2. Private Method
#incrementAccess() {
this.#accessCount++;
console.log(`Access #${this.#accessCount}`);
}
unlock(inputKey) {
if (inputKey === this.#secretKey) {
this.#incrementAccess();
return 'Sensors Activated';
}
return 'Access Denied';
}
}
const vault = new SecureVault('BLUE-BIRD');
console.log(vault.unlock('BLUE-BIRD'));
// console.log(vault.#secretKey); // ⌠Uncaught SyntaxErrorLegacy Privacy: The WeakMap Pattern
Before the # syntax, developers usedWeakMap to store private data. This pattern associates private state with a specific object instance without preventing garbage collection, ensuring memory efficiency even for large sets of objects.
// --- The WeakMap Pattern (Legacy "Hard" Privacy) ---
const _internalState = new WeakMap();
class UserAccount {
constructor(userId) {
_internalState.set(this, {
userId: userId,
loginTime: Date.now()
});
}
getDetails() {
const state = _internalState.get(this);
return `User: ${state.userId} logged at ${state.loginTime}`;
}
}
// Memory Efficient: When the UserAccount instance is garbage
// collected, the WeakMap entry is also removed automatically.Encapsulated State Machines
The most powerful use case for private fields isInternal State Management. By making the status field private, you force all updates to go through a valid transition method, preventing objects from entering impossible states.
// --- Private State Machine Pattern ---
class Transaction {
#state = 'PENDING'; // Truly encapsulated state
#transitions = {
'PENDING': ['AUTHORIZED', 'CANCELLED'],
'AUTHORIZED': ['CAPTURED', 'REFUNDED'],
};
process(nextState) {
const allowed = this.#transitions[this.#state];
if (allowed?.includes(nextState)) {
this.#state = nextState;
console.log(`Status: ${this.#state}`);
} else {
throw new Error(`Invalid move: ${this.#state} -> ${nextState}`);
}
}
}
const tx = new Transaction();
tx.process('AUTHORIZED');
// tx.#state = 'CAPTURED'; // ⌠Cannot bypass the process() logicSenior Architect's Checklist:
- ✅ Hard Privacy: Use
#for data that should NEVER be touched by external consumers. - ✅ Mandatory Declaration: Unlike public fields, private fields MUST be declared in the class body.
- ⌠Inheritance: Remember that private fields are NOT visible to subclasses (even
supercannot see them). - ✅ Performance: Modern engines (V8) can optimize private field access because the list of properties is fixed at compile-time.
- ✅ Refactoring: Use privacy to hide the "messy" internals of your API, keeping the public interface clean.