Docs
11.4-Private-Fields
8.4 Private Fields & Methods
Overview
ES2022 introduced true private class members using the # prefix. Private fields and methods are only accessible within the class body.
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā MyClass ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā Public Members: ā
ā āā this.publicProp (accessible) ā
ā āā this.publicMethod() (accessible) ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā Private Members (#): ā
ā āā this.#privateProp (hidden) ā
ā āā this.#privateMethod (hidden) ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā ā
Accessible SyntaxError
from outside if accessed
Private Instance Fields
class BankAccount {
#balance = 0; // Private field
#pin; // Private field (undefined initially)
constructor(initialBalance, pin) {
this.#balance = initialBalance;
this.#pin = pin;
}
deposit(amount) {
if (amount > 0) {
this.#balance += amount;
}
}
withdraw(amount, pin) {
if (!this.#verifyPin(pin)) {
throw new Error('Invalid PIN');
}
if (amount > this.#balance) {
throw new Error('Insufficient funds');
}
this.#balance -= amount;
return amount;
}
// Private method
#verifyPin(pin) {
return this.#pin === pin;
}
get balance() {
return this.#balance;
}
}
const account = new BankAccount(1000, '1234');
console.log(account.balance); // 1000 (via getter)
// console.log(account.#balance); // SyntaxError: Private field
Private vs Public Comparison
āāāāāāāāāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāāāāāāāāā
ā Aspect ā Public ā Private (#) ā
āāāāāāāāāāāāāāāāāāāāāā¼āāāāāāāāāāāāāāāāāāāāāā¼āāāāāāāāāāāāāāāāāāāāāā¤
ā Syntax ā this.prop ā this.#prop ā
ā Declaration ā Optional ā Required in class ā
ā External Access ā Yes ā No (SyntaxError) ā
ā Reflection ā Yes (Object.keys) ā No ā
ā Subclass Access ā Yes ā No ā
ā Performance ā Standard ā Slightly faster ā
ā Debugging ā Visible ā Hidden in console ā
āāāāāāāāāāāāāāāāāāāāāā“āāāāāāāāāāāāāāāāāāāāāā“āāāāāāāāāāāāāāāāāāāāāā
Private Methods
class User {
#passwordHash;
constructor(username, password) {
this.username = username;
this.#passwordHash = this.#hashPassword(password);
}
// Private method
#hashPassword(password) {
// Simplified hash (use proper crypto in production)
return Buffer.from(password).toString('base64');
}
// Private validation
#validatePassword(password) {
return this.#hashPassword(password) === this.#passwordHash;
}
// Public interface
authenticate(password) {
return this.#validatePassword(password);
}
changePassword(oldPassword, newPassword) {
if (!this.#validatePassword(oldPassword)) {
throw new Error('Invalid current password');
}
this.#passwordHash = this.#hashPassword(newPassword);
}
}
Private Static Members
class Config {
// Private static field
static #settings = new Map();
static #initialized = false;
// Private static method
static #load() {
if (this.#initialized) return;
this.#settings.set('theme', 'light');
this.#settings.set('language', 'en');
this.#initialized = true;
}
// Public static interface
static get(key) {
this.#load();
return this.#settings.get(key);
}
static set(key, value) {
this.#load();
this.#settings.set(key, value);
}
}
console.log(Config.get('theme')); // "light"
// Config.#settings; // SyntaxError
Private Getters and Setters
class Temperature {
#celsius;
constructor(celsius) {
this.#celsius = celsius;
}
// Private getter
get #kelvin() {
return this.#celsius + 273.15;
}
// Private setter
set #kelvin(value) {
this.#celsius = value - 273.15;
}
// Public interface
get celsius() {
return this.#celsius;
}
set celsius(value) {
this.#celsius = value;
}
get fahrenheit() {
return (this.#celsius * 9) / 5 + 32;
}
// Internal use of private getter
toKelvin() {
return this.#kelvin;
}
}
Checking for Private Fields
Use in operator to check if private field exists:
class User {
#id;
constructor(id) {
this.#id = id;
}
static isUser(obj) {
// Check if #id exists in obj
return #id in obj;
}
}
const user = new User(1);
console.log(User.isUser(user)); // true
console.log(User.isUser({})); // false
Private Fields in Inheritance
Private fields are NOT accessible in subclasses:
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā Parent ā
ā āā this.publicProp ā ā
ā āā this._protectedProp ā (convention) ā
ā āā this.#privateProp ā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā extends
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā Child ā
ā āā Access this.publicProp ā ā
ā āā Access this._protectedProp ā ā
ā āā Access this.#privateProp ā ā
ā āā Must use parent's methods ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
class Parent {
#secret = 'hidden';
_protected = 'accessible'; // Convention
getSecret() {
return this.#secret; // Accessor method
}
}
class Child extends Parent {
showData() {
// console.log(this.#secret); // SyntaxError
console.log(this._protected); // Works (convention)
console.log(this.getSecret()); // Works (via method)
}
}
WeakMap Alternative (Pre-ES2022)
Before private fields, WeakMap was used:
// Old pattern (still works)
const _balance = new WeakMap();
const _pin = new WeakMap();
class BankAccountOld {
constructor(balance, pin) {
_balance.set(this, balance);
_pin.set(this, pin);
}
get balance() {
return _balance.get(this);
}
withdraw(amount, pin) {
if (_pin.get(this) !== pin) {
throw new Error('Invalid PIN');
}
const current = _balance.get(this);
_balance.set(this, current - amount);
}
}
// Modern pattern (ES2022+)
class BankAccountNew {
#balance;
#pin;
constructor(balance, pin) {
this.#balance = balance;
this.#pin = pin;
}
get balance() {
return this.#balance;
}
}
Private Fields vs Symbol Properties
āāāāāāāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāāāāāāāā
ā Aspect ā Private Fields (#) ā Symbol Properties ā
āāāāāāāāāāāāāāāāāāāā¼āāāāāāāāāāāāāāāāāāāāā¼āāāāāāāāāāāāāāāāāāāāā¤
ā Truly Private ā Yes ā No (discoverable) ā
ā Inheritance ā No access ā Inherited ā
ā Reflection ā Not visible ā Object.getOwn... ā
ā Proxy Support ā Tricky ā Works normally ā
ā String Key ā No ā Via Symbol.for ā
āāāāāāāāāāāāāāāāāāāā“āāāāāāāāāāāāāāāāāāāāā“āāāāāāāāāāāāāāāāāāāāā
Common Patterns
Encapsulating State
class Counter {
#count = 0;
#max;
#min;
constructor({ min = 0, max = Infinity } = {}) {
this.#min = min;
this.#max = max;
this.#count = min;
}
increment() {
if (this.#count < this.#max) {
this.#count++;
}
return this;
}
decrement() {
if (this.#count > this.#min) {
this.#count--;
}
return this;
}
get value() {
return this.#count;
}
}
Immutable-like Objects
class Point {
#x;
#y;
constructor(x, y) {
this.#x = x;
this.#y = y;
Object.freeze(this); // Prevent adding properties
}
get x() {
return this.#x;
}
get y() {
return this.#y;
}
// Returns new instance instead of mutating
add(other) {
return new Point(this.#x + other.x, this.#y + other.y);
}
scale(factor) {
return new Point(this.#x * factor, this.#y * factor);
}
}
Best Practices
| Do ā | Don't ā |
|---|---|
Use # for true encapsulation | Expose internal state unnecessarily |
| Provide public accessors when needed | Rely solely on underscore convention |
| Use private methods for internal logic | Make everything private |
Check #field in obj for type checking | Try to access private fields externally |
| Document public API clearly | Forget that subclasses can't access |
Key Takeaways
- ā¢
#prefix creates truly private members - ā¢Private fields must be declared in the class body
- ā¢No external access - SyntaxError if tried
- ā¢No inheritance access - use public methods
- ā¢
#field in objchecks for private field existence - ā¢Private statics work the same way
- ā¢WeakMap is the pre-ES2022 alternative