Master Data Hiding and Private Variables
Encapsulation is the practice of bundling data and methods that operate on that data within a single unit, while hiding the internal implementation details from the outside world. It's one of the four fundamental principles of object-oriented programming (along with inheritance, polymorphism, and abstraction).
In JavaScript, encapsulation helps you:
// Without encapsulation - data is exposed
const user = {
balance: 1000
};
user.balance = -500; // Oops! No validation
// With encapsulation - data is protected
function createUser(initialBalance) {
let balance = initialBalance; // Private variable
return {
getBalance() {
return balance;
},
deposit(amount) {
if (amount > 0) {
balance += amount;
return true;
}
return false;
},
withdraw(amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
return true;
}
return false;
}
};
}
const account = createUser(1000);
console.log(account.getBalance()); // 1000
account.deposit(500); // true
console.log(account.getBalance()); // 1500
account.balance = -500; // Has no effect!
console.log(account.getBalance()); // Still 1500
Before ES2022, closures were the primary way to create private variables in JavaScript. A closure allows an inner function to access variables from its outer function, even after the outer function has returned.
function Counter() {
let count = 0; // Private variable
this.increment = function() {
count++;
return count;
};
this.decrement = function() {
count--;
return count;
};
this.getCount = function() {
return count;
};
}
const counter = new Counter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.getCount()); // 2
console.log(counter.count); // undefined - private!
The count variable is only accessible through the methods we've defined. There's no way to directly access or modify it from outside.
The Module Pattern uses an Immediately Invoked Function Expression (IIFE) to create a private scope and return a public API:
const Calculator = (function() {
// Private variables and functions
let history = [];
function log(operation, result) {
history.push({ operation, result, timestamp: Date.now() });
}
// Public API
return {
add(a, b) {
const result = a + b;
log(`${a} + ${b}`, result);
return result;
},
subtract(a, b) {
const result = a - b;
log(`${a} - ${b}`, result);
return result;
},
getHistory() {
return [...history]; // Return copy, not reference
},
clearHistory() {
history = [];
}
};
})();
console.log(Calculator.add(5, 3)); // 8
console.log(Calculator.subtract(10, 4)); // 6
console.log(Calculator.getHistory()); // Array with 2 operations
console.log(Calculator.history); // undefined - private!
The Module Pattern is excellent for creating singleton objects with private state and a clean public interface.
Modern JavaScript (ES2022) introduced true private fields using the # prefix. These are enforced by the JavaScript engine itself:
class BankAccount {
#balance; // Private field
#transactions; // Private field
constructor(initialBalance) {
this.#balance = initialBalance;
this.#transactions = [];
}
deposit(amount) {
if (amount <= 0) {
throw new Error('Amount must be positive');
}
this.#balance += amount;
this.#logTransaction('deposit', amount);
return this.#balance;
}
withdraw(amount) {
if (amount <= 0) {
throw new Error('Amount must be positive');
}
if (amount > this.#balance) {
throw new Error('Insufficient funds');
}
this.#balance -= amount;
this.#logTransaction('withdraw', amount);
return this.#balance;
}
getBalance() {
return this.#balance;
}
// Private method
#logTransaction(type, amount) {
this.#transactions.push({
type,
amount,
balance: this.#balance,
timestamp: new Date()
});
}
getTransactionHistory() {
return [...this.#transactions]; // Return copy
}
}
const account = new BankAccount(1000);
account.deposit(500);
console.log(account.getBalance()); // 1500
// account.#balance; // SyntaxError!
// account.#logTransaction('hack', 999); // SyntaxError!
Private fields are truly private - attempting to access them from outside the class results in a syntax error, not just undefined.
Before private fields, WeakMaps were used to store truly private data:
const privateData = new WeakMap();
class User {
constructor(name, email, password) {
// Store private data in WeakMap
privateData.set(this, {
password: password,
loginAttempts: 0
});
// Public properties
this.name = name;
this.email = email;
}
login(password) {
const data = privateData.get(this);
if (password === data.password) {
data.loginAttempts = 0;
return true;
}
data.loginAttempts++;
if (data.loginAttempts >= 3) {
throw new Error('Account locked');
}
return false;
}
changePassword(oldPassword, newPassword) {
const data = privateData.get(this);
if (oldPassword !== data.password) {
throw new Error('Incorrect password');
}
data.password = newPassword;
return true;
}
}
const user = new User('Alice', 'alice@example.com', 'secret123');
console.log(user.name); // 'Alice'
console.log(user.password); // undefined - private!
user.login('wrong'); // false
user.login('secret123'); // true
WeakMaps are still useful when you need to attach private data to objects you don't control, but private fields are now the preferred approach for classes.
Getters and setters provide controlled access to private data with validation:
class Temperature {
#celsius;
constructor(celsius) {
this.celsius = celsius; // Uses setter
}
get celsius() {
return this.#celsius;
}
set celsius(value) {
if (typeof value !== 'number') {
throw new Error('Temperature must be a number');
}
if (value < -273.15) {
throw new Error('Temperature below absolute zero');
}
this.#celsius = value;
}
get fahrenheit() {
return (this.#celsius * 9/5) + 32;
}
set fahrenheit(value) {
this.celsius = (value - 32) * 5/9; // Uses celsius setter
}
get kelvin() {
return this.#celsius + 273.15;
}
set kelvin(value) {
this.celsius = value - 273.15; // Uses celsius setter
}
}
const temp = new Temperature(25);
console.log(temp.celsius); // 25
console.log(temp.fahrenheit); // 77
console.log(temp.kelvin); // 298.15
temp.fahrenheit = 32;
console.log(temp.celsius); // 0
// temp.celsius = -300; // Error: Temperature below absolute zero
Let's build a shopping cart with proper encapsulation:
class ShoppingCart {
#items;
#discountRate;
constructor() {
this.#items = [];
this.#discountRate = 0;
}
addItem(product, quantity = 1) {
if (quantity <= 0) {
throw new Error('Quantity must be positive');
}
const existingItem = this.#items.find(item => item.product.id === product.id);
if (existingItem) {
existingItem.quantity += quantity;
} else {
this.#items.push({ product, quantity });
}
return this.#calculateTotal();
}
removeItem(productId) {
const index = this.#items.findIndex(item => item.product.id === productId);
if (index !== -1) {
this.#items.splice(index, 1);
}
return this.#calculateTotal();
}
updateQuantity(productId, quantity) {
if (quantity <= 0) {
return this.removeItem(productId);
}
const item = this.#items.find(item => item.product.id === productId);
if (item) {
item.quantity = quantity;
}
return this.#calculateTotal();
}
applyDiscount(percentage) {
if (percentage < 0 || percentage > 100) {
throw new Error('Discount must be between 0 and 100');
}
this.#discountRate = percentage / 100;
return this.#calculateTotal();
}
#calculateTotal() {
const subtotal = this.#items.reduce((sum, item) => {
return sum + (item.product.price * item.quantity);
}, 0);
const discount = subtotal * this.#discountRate;
return subtotal - discount;
}
getItems() {
// Return deep copy to prevent external modification
return this.#items.map(item => ({
product: { ...item.product },
quantity: item.quantity
}));
}
getTotal() {
return this.#calculateTotal();
}
getItemCount() {
return this.#items.reduce((sum, item) => sum + item.quantity, 0);
}
clear() {
this.#items = [];
this.#discountRate = 0;
}
}
// Usage
const cart = new ShoppingCart();
cart.addItem({ id: 1, name: 'Laptop', price: 999 }, 1);
cart.addItem({ id: 2, name: 'Mouse', price: 25 }, 2);
console.log(cart.getTotal()); // 1049
console.log(cart.getItemCount()); // 3
cart.applyDiscount(10); // 10% off
console.log(cart.getTotal()); // 944.10
// cart.#items.push(...); // SyntaxError - can't access private field!
// cart.#discountRate = 0.5; // SyntaxError - can't access private field!
Only expose what's necessary for the public API:
class DataProcessor {
#data;
#cache;
constructor(data) {
this.#data = data;
this.#cache = new Map();
}
// Public method
process() {
return this.#validate() && this.#transform() && this.#save();
}
// Private methods
#validate() { /* ... */ }
#transform() { /* ... */ }
#save() { /* ... */ }
}
class Rectangle {
#width;
#height;
constructor(width, height) {
this.#width = width;
this.#height = height;
}
get area() {
return this.#width * this.#height;
}
get perimeter() {
return 2 * (this.#width + this.#height);
}
}
class Person {
#age;
set age(value) {
if (typeof value !== 'number' || value < 0 || value > 150) {
throw new Error('Invalid age');
}
this.#age = value;
}
get age() {
return this.#age;
}
}
class Team {
#members;
constructor() {
this.#members = [];
}
getMembers() {
// Return copy to prevent external modification
return [...this.#members];
}
}
// Good - clear intent
class User {
#isActive;
activate() { this.#isActive = true; }
deactivate() { this.#isActive = false; }
isActive() { return this.#isActive; }
}
// Bad - unclear
class User {
#status;
set(val) { this.#status = val; }
get() { return this.#status; }
}
Use encapsulation when:
Consider simpler approaches when:
// Bad - too much encapsulation for a simple point
class Point {
#x;
#y;
getX() { return this.#x; }
setX(x) { this.#x = x; }
getY() { return this.#y; }
setY(y) { this.#y = y; }
}
// Better - simple data structure
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
// Bad - exposes internal array
class List {
#items = [];
getItems() {
return this.#items; // External code can modify this!
}
}
// Good - returns copy
class List {
#items = [];
getItems() {
return [...this.#items];
}
}
// Bad - mixing patterns
class User {
#name;
email; // Public
getName() { return this.#name; }
}
// Good - consistent
class User {
#name;
#email;
getName() { return this.#name; }
getEmail() { return this.#email; }
}