JavaScript Private & Public Fields
Control access to object properties with true encapsulation using #.
Public Fields
Declared directly in the class body — accessible from outside:
class User {
name;
email;
constructor(name, email) { this.name = name; this.email = email; }
}
let user = new User("John", "[email protected]");
console.log(user.name); // "John"
Private Fields
Prefix with # — completely inaccessible outside the class:
class BankAccount {
#balance;
#pin;
constructor(owner, initialBalance, pin) {
this.owner = owner;
this.#balance = initialBalance;
this.#pin = pin;
}
getBalance(pin) {
if (pin !== this.#pin) throw new Error("Invalid PIN");
return this.#balance;
}
deposit(amount, pin) {
if (pin !== this.#pin) throw new Error("Invalid PIN");
this.#balance += amount;
return this;
}
}
let account = new BankAccount("John", 1000, "1234");
console.log(account.getBalance("1234")); // 1000
// console.log(account.#balance); // Syntax Error!
Remember: # fields throw a SyntaxError if accessed from outside the class.
Private Methods
Internal helpers hidden from the outside:
class PasswordValidator {
#password;
constructor(pw) { this.#password = pw; }
#hasMinLength(p) { return p.length >= 8; }
#hasUppercase(p) { return /[A-Z]/.test(p); }
#hasNumber(p) { return /[0-9]/.test(p); }
validate() {
let errors = [];
if (!this.#hasMinLength(this.#password)) errors.push("Min 8 characters");
if (!this.#hasUppercase(this.#password)) errors.push("Need uppercase");
if (!this.#hasNumber(this.#password)) errors.push("Need a number");
return { valid: errors.length === 0, errors };
}
}
let v = new PasswordValidator("Secret123!");
console.log(v.validate()); // { valid: true, errors: [] }
Public vs Private — When to Use Which
class Product {
#name; // Private: needs validation
#price; // Private: must be non-negative
category; // Public: safe to access directly
constructor(name, price, category) {
this.name = name;
this.price = price;
this.category = category;
}
get name() { return this.#name; }
set name(v) { if (!v || v.length < 2) throw new Error("Name too short"); this.#name = v; }
get price() { return this.#price; }
set price(v) { if (v < 0) throw new Error("Negative price"); this.#price = v; }
}
Rule of thumb: Private if it needs validation. Public if safe to access directly.
Complete Example: Cache
class Cache {
#cache = new Map();
#maxSize;
#ttl;
constructor(maxSize = 100, ttl = 60000) { this.#maxSize = maxSize; this.#ttl = ttl; }
#isExpired(e) { return Date.now() - e.timestamp > this.#ttl; }
#evict() {
if (this.#cache.size >= this.#maxSize) {
this.#cache.delete(this.#cache.keys().next().value);
}
}
set(key, value) {
this.#evict();
this.#cache.set(key, { value, timestamp: Date.now() });
return this;
}
get(key) {
let e = this.#cache.get(key);
if (!e || this.#isExpired(e)) { this.#cache.delete(key); return null; }
return e.value;
}
has(key) { return this.get(key) !== null; }
get size() { return this.#cache.size; }
}
let cache = new Cache(3, 5000);
cache.set("a", 1).set("b", 2).set("c", 3);
cache.set("d", 4); // Evicts "a"
console.log(cache.get("a")); // null
Best Practices
- Use private for sensitive data — Balances, passwords, keys
- Use public for the API surface — Safe to access directly
- Provide public methods for controlled access — Validate before modifying
- Don’t over-encapsulate — Not every field needs to be private
Common Mistakes
- Forgetting
#— Private fields require the hash prefix - Over-encapsulating — Too many privates clutter the API
- Not providing access — Private data with no way to use it is useless
- Returning mutable objects from getters — Breaks encapsulation