Public vs Private Access
Introduction
One of the core principles of object-oriented programming is encapsulation — hiding the internal details of a class and only exposing what’s necessary.
In JavaScript, properties and methods are public by default, meaning they can be accessed from outside the class. But modern JavaScript (ES2022+) introduces private fields and methods using the # prefix.
Encapsulation helps:
- Prevent accidental modification of sensitive data.
- Control how data is read or changed (using getters/setters).
- Keep internal logic hidden while exposing a clean interface.
Concepts
Public Members
- Default in JavaScript.
- Accessible anywhere.
class Person {
constructor(name) {
this.name = name; // public
}
}
const p = new Person("Alice");
console.log(p.name); // Alice ✅
Private Fields
- Start with #.
- Only accessible inside the class.
- Attempting to use them outside results in an error.
class BankAccount {
#balance;
constructor(owner, startingBalance = 0) {
this.owner = owner; // public
this.#balance = startingBalance; // private
}
deposit(amount) {
this.#balance += amount;
}
getBalance() {
return this.#balance;
}
}
const acc = new BankAccount("Alice", 100);
acc.deposit(50);
console.log(acc.getBalance()); // 150
// console.log(acc.#balance); ❌ Error
Getters and Setters
- Provide controlled access to private fields.
class Employee {
#salary;
constructor(name, salary) {
this.name = name;
this.#salary = salary;
}
get salary() {
return this.#salary;
}
set salary(value) {
if (value < 0) throw new Error("Salary must be positive");
this.#salary = value;
}
}
const emp = new Employee("Bob", 50000);
console.log(emp.salary); // 50000
emp.salary = 60000;
console.log(emp.salary); // 60000
Private Methods
- Also use #.
- Useful for internal helpers.
class SafeBox {
#pin;
constructor(pin) {
this.#pin = pin;
}
#verifyPin(input) {
return input === this.#pin;
}
unlock(input) {
if (this.#verifyPin(input)) {
console.log("Unlocked!");
} else {
console.log("Access denied");
}
}
}
const box = new SafeBox("1234");
box.unlock("1234"); // ✅ Unlocked!
box.unlock("0000"); // ❌ Access denied
// box.#verifyPin("1234"); ❌ Error
Challenges
1. User class with public and private properties
- Create a User class with a public property username and a private property #password.
- Add a method checkPassword(input) to verify it.
💡 Show Answer
class User {
constructor(username, password) {
this.username = username;
this.#password = password;
}
#password;
checkPassword(input) {
return input === this.#password;
}
}
// Example usage:
const user = new User("alice", "secret123");
console.log(user.checkPassword("secret123")); // true
console.log(user.checkPassword("wrong")); // false
2. User with private password
- Modify the User class to include a
setPassword(newPassword)method that updates the password only if it meets certain rules (e.g., at least 6 characters).
💡 Show Answer
class User {
#password;
constructor(username, password) {
this.username = username;
this.#password = password;
}
checkPassword(input) {
return input === this.#password;
}
setPassword(newPassword) {
if (typeof newPassword === 'string' && newPassword.length >= 6) {
this.#password = newPassword;
return true;
}
return false;
}
}
// Example usage:
const user = new User("alice", "secret123");
console.log(user.checkPassword("secret123")); // true
console.log(user.setPassword("short")); // false
console.log(user.setPassword("newpass123")); // true
console.log(user.checkPassword("newpass123")); // true
3. Attempt direct access
- Create a
BankAccountclass with publicownerand private#balance. - Add methods
deposit(), withdraw(), andgetBalance().
💡 Show Answer
class BankAccount {
owner;
#balance;
constructor(owner, initialBalance = 0) {
this.owner = owner;
this.#balance = initialBalance;
}
deposit(amount) {
if (amount > 0) {
this.#balance += amount;
return true;
}
return false;
}
withdraw(amount) {
if (amount > 0 && amount <= this.#balance) {
this.#balance -= amount;
return true;
}
return false;
}
getBalance() {
return this.#balance;
}
}
const account = new BankAccount("Alice", 100);
account.deposit(50);
account.withdraw(30);
console.log(account.getBalance()); // 120
// console.log(account.#balance); // SyntaxError
4. Wallet with deposit/withdraw
- Demonstrate what happens if you try to access a private property directly.
💡 Show Answer
const account = new BankAccount("Alice", 100);
console.log(account.#balance); // SyntaxError: Private field '#balance' must be declared in an enclosing class
5. Car with private mileage
- Write a
Carclass with a private property#mileageand public methoddrive(km). - Add a
getMileage()method to read it safely.
💡 Show Answer
class Car {
#mileage = 0;
drive(km) {
if (typeof km !== "number" || km < 0) {
throw new Error("Distance must be a non-negative number");
}
this.#mileage += km;
}
getMileage() {
return this.#mileage;
}
}
const myCar = new Car();
myCar.drive(150);
console.log(myCar.getMileage());
6. Employee with private salary
- Create an Employee class with a private salary field and public getter/setter.
- Validate that salary cannot be negative.
💡 Show Answer
class Employee {
#salary;
constructor(salary = 0) {
this.salary = salary; // Use setter for validation
}
get salary() {
return this.#salary;
}
set salary(value) {
if (typeof value !== 'number' || value < 0) {
throw new Error('Salary must be a non-negative number.');
}
this.#salary = value;
}
}
const emp = new Employee(50000);
console.log(emp.salary); // 50000
7. Library with private books
- Build a
SafeBoxclass with a private PIN and a private method#verifyPin(). - Add a public method
unlock(pin)that uses it.
💡 Show Answer
class SafeBox {
#pin;
constructor(pin) {
this.#pin = pin;
}
#verifyPin(inputPin) {
return inputPin === this.#pin;
}
unlock(pin) {
if (this.#verifyPin(pin)) {
return "SafeBox unlocked!";
} else {
return "Incorrect PIN.";
}
}
}
// Example usage:
const box = new SafeBox(1234);
console.log(box.unlock(1234)); // "SafeBox unlocked!"
console.log(box.unlock(1111)); // "Incorrect PIN."
8. Encapsulation example
- Write a class with both a private method and a private field, and show how they interact.
💡 Show Answer
class Example {
#privateField;
constructor(value) {
this.#privateField = value;
}
#privateMethod() {
return `Private field value is: ${this.#privateField}`;
}
publicMethod() {
return this.#privateMethod();
}
}
// Usage
const obj = new Example('Hello, world!');
console.log(obj.publicMethod()); // Output: Private field value is: Hello, world!
9. Public id + private secret
- Create a
LoginSystemclass with private fields#attemptsand#locked. - Add a method that locks the account after 3 failed attempts.
💡 Show Answer
class LoginSystem {
#attempts = 0;
#locked = false;
login(password, correctPassword) {
if (this.#locked) {
return "Account is locked.";
}
if (password === correctPassword) {
this.#attempts = 0;
return "Login successful!";
} else {
this.#attempts++;
if (this.#attempts >= 3) {
this.#locked = true;
return "Account locked due to too many failed attempts.";
}
return `Incorrect password. Attempt ${this.#attempts}/3.`;
}
}
isLocked() {
return this.#locked;
}
reset() {
this.#attempts = 0;
this.#locked = false;
}
}
// Example usage:
const loginSystem = new LoginSystem();
console.log(loginSystem.login("wrong1", "correct")); // Incorrect password. Attempt 1/3.
console.log(loginSystem.login("wrong2", "correct")); // Incorrect password. Attempt 2/3.
console.log(loginSystem.login("wrong3", "correct")); // Account locked due to too many failed attempts.
console.log(loginSystem.login("correct", "correct")); // Account is locked.
console.log(loginSystem.isLocked()); // true
loginSystem.reset();
console.log(loginSystem.login("correct", "correct")); // Login successful!
console.log(loginSystem.isLocked()); // false
10. Student grade private
- Create a
Configclass with a private static property#settings(an object). - Add public static methods
set(key, value)andget(key)to manage it.
💡 Show Answer
class Config {
static #settings = {};
static set(key, value) {
Config.#settings[key] = value;
}
static get(key) {
return Config.#settings[key];
}
}
// Example usage:
Config.set("theme", "dark");
console.log(Config.get("theme")); // Output: dark
Config.set("language", "en");
console.log(Config.get("language")); // Output: en
console.log(Config.get("nonexistent")); // Output: undefined