JavaScript 28 🧬 Encapsulation
Encapsulation is one of the fundamental concepts in object-oriented programming (OOP). It’s used to restrict access to some of an object’s internal state — protecting data integrity, managing complexity, and promoting maintainability by allowing controlled access to internal properties and behaviors.
A Quick Look at the Examples
// 1. With closures
function Counter() {
let count = 0;
return {
increment: function () { count++; },
decrement: function () { count--; },
getCount: function () { return count; }
};
}
const counter = Counter();
counter.increment();
counter.increment();
console.log(counter.getCount()); // 2
console.log(counter.count); // undefined (private!)
// 2. With ES6 classes and private fields
class BankAccount {
#balance = 0;
constructor(initialBalance) {
this.#balance = initialBalance;
}
deposit(amount) {
if (amount < 0) throw new Error("Cannot deposit negative amount");
this.#balance += amount;
}
withdraw(amount) {
if (amount > this.#balance) throw new Error("Insufficient balance");
this.#balance -= amount;
}
getBalance() {
return this.#balance;
}
}
const account = new BankAccount(1000);
account.deposit(500);
account.withdraw(200);
console.log(account.getBalance()); // 1300
// console.log(account.#balance); // ❌ SyntaxError: private field
// 3. With getters and setters
class Person {
#name;
constructor(name) {
this.#name = name;
}
get name() {
return this.#name;
}
set name(newName) {
if (newName.length < 2) {
throw new Error("Name must be at least 2 characters");
}
this.#name = newName;
}
}
const person = new Person("Alice");
console.log(person.name); // "Alice"
// person.name = "A"; // ❌ Error: Name must be at least 2 characters
// 4. With private module scope
let count = 0; // Not exported — private!
export function increment() { count++; }
export function decrement() { count--; }
export function getCount() { return count; }
// 5. With IIFE (Immediately Invoked Function Expression)
const Counter2 = (function () {
let count = 0;
function increment() { count++; }
function decrement() { count--; }
function getCount() { return count; }
return { increment, decrement, getCount };
})();
Counter2.increment();
Counter2.increment();
console.log(Counter2.getCount()); // 2
console.log(Counter2.count); // undefined (private!)
a. What is Encapsulation?
Encapsulation is the practice of restricting direct access to an object’s internal state. It allows controlled access through methods, protecting data from unintended modification.
Core Concepts
| Concept | Description |
|---|---|
| Data Hiding | Hide the internal state (variables) to prevent unintended modifications |
| Access Control | Provide controlled access (via methods) to the internal state |
| Abstraction | Expose only the necessary parts of the object’s behavior, hiding implementation details |
Why Encapsulation Matters
| Benefit | Description |
|---|---|
| Security | Prevents unauthorized changes to internal state |
| Maintainability | Makes code easier to modify and debug |
| Reusability | Encapsulated code is more modular and reusable |
| Scalability | Reduces dependencies and makes large systems manageable |
b. How to Achieve Encapsulation — Part 1
1. Using Closures (Function Scopes)
Closures allow you to create private variables and functions that are not accessible outside the scope of the function.
function Counter() {
let count = 0; // Private — inaccessible from outside
return {
increment: function () { count++; },
decrement: function () { count--; },
getCount: function () { return count; }
};
}
const counter = Counter();
counter.increment();
counter.increment();
console.log(counter.getCount()); // 2
console.log(counter.count); // undefined — private!
Advantages:
- ✅ Strong data privacy
- ✅ Prevents direct tampering with internal state
How it works:
Counter()
│
▼
┌─────────────────────┐
│ let count = 0; │ ← Private variable
│ │
│ return { │
│ increment, │ ← Public methods
│ decrement, │ (closures over count)
│ getCount │
│ } │
└─────────────────────┘
2. Using ES6 Classes with Private Fields (#)
ES6 introduced private class fields, denoted by #, which cannot be accessed from outside the class.
class BankAccount {
#balance = 0; // Private field
constructor(initialBalance) {
this.#balance = initialBalance;
}
deposit(amount) {
if (amount < 0) throw new Error("Cannot deposit negative amount");
this.#balance += amount;
}
withdraw(amount) {
if (amount > this.#balance) throw new Error("Insufficient balance");
this.#balance -= amount;
}
getBalance() {
return this.#balance;
}
}
const account = new BankAccount(1000);
account.deposit(500);
account.withdraw(200);
console.log(account.getBalance()); // 1300
// console.log(account.#balance); // ❌ SyntaxError
Advantages:
- ✅ Native support for private fields
- ✅ Clean, readable, modern syntax
- ✅ Enforced by the language (true privacy)
3. Using ES6 Classes with Getters/Setters
Use get and set methods to control access to internal state.
class Person {
#name;
constructor(name) {
this.#name = name;
}
get name() {
return this.#name;
}
set name(newName) {
if (newName.length < 2) {
throw new Error("Name must be at least 2 characters");
}
this.#name = newName;
}
}
const person = new Person("Alice");
console.log(person.name); // "Alice" — uses getter
person.name = "Bob"; // Uses setter (valid)
console.log(person.name); // "Bob"
// person.name = "A"; // ❌ Error: Name must be at least 2 characters
Advantages:
- ✅ Enforces validation and constraints
- ✅ Encapsulates logic for reading and writing data
- ✅ Same syntax as regular properties
c. How to Achieve Encapsulation — Part 2
4. Using ES6 Modules (Private Module Scope)
Modules allow you to expose only certain parts of your code while keeping the rest private.
// counter.js
let count = 0; // Not exported — private!
export function increment() { count++; }
export function decrement() { count--; }
export function getCount() { return count; }
// main.js
import { increment, getCount } from './counter.js';
increment();
increment();
console.log(getCount()); // 2
// console.log(count); // ❌ ReferenceError: count is not defined
Advantages:
- ✅ Clean separation of public and private code
- ✅ Encourages modular and reusable code
5. Using IIFE (Immediately Invoked Function Expression)
IIFEs create a private scope and expose only the necessary methods.
const Counter = (function () {
let count = 0; // Private
function increment() { count++; }
function decrement() { count--; }
function getCount() { return count; }
return {
increment,
decrement,
getCount
};
})();
Counter.increment();
Counter.increment();
console.log(Counter.getCount()); // 2
console.log(Counter.count); // undefined — private!
Advantages:
- ✅ Works in older JavaScript environments (pre-ES6)
- ✅ Encapsulates state and logic in a single scope
How it works:
(function () {
let count = 0; // Private scope
return { ... }; // Only these are exposed
})();
│
▼
Immediately invoked — runs once, returns the public API
When to Use Which Approach
| Approach | Best For | Notes |
|---|---|---|
| Closures | Small, standalone utilities | Not suitable for large-scale OOP |
| ES6 Classes (private #) | Modern, scalable apps | Supports class-based OOP |
| Getters/Setters | Fine-grained data control | Enhances readability and safety |
| ES6 Modules | Modular applications | Ideal for large projects |
| IIFE | Legacy environments or libraries | Less readable than ES6 classes |
Comparison of Approaches
| Approach | True Privacy? | Modern? | Complexity | Best For |
|---|---|---|---|---|
| Closures | ✅ Yes | ✅ Yes | Low | Small utilities, factories |
ES6 # Private | ✅ Yes (enforced) | ✅ Yes | Low | Modern OOP |
| Getters/Setters | ❌ (with # yes) | ✅ Yes | Low | Validation, computed props |
| Modules | ✅ Yes | ✅ Yes | Low | Multi-file apps |
| IIFE | ✅ Yes | ⚠️ Legacy | Medium | Pre-ES6 code, libraries |
Complete Example
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Encapsulation</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background: #f8f9fa;
color: #333;
line-height: 1.6;
}
h1 { color: #007bff; border-bottom: 3px solid #007bff; padding-bottom: 10px; }
h2 { color: #28a745; border-left: 4px solid #28a745; padding-left: 15px; margin-top: 30px; }
.demo-box {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
margin: 15px 0;
}
pre {
background: #1e1e1e;
color: #d4d4d4;
padding: 15px;
border-radius: 8px;
overflow-x: auto;
font-family: 'Courier New', monospace;
font-size: 0.9rem;
line-height: 1.8;
}
.keyword { color: #569cd6; }
.string { color: #ce9178; }
.number { color: #b5cea8; }
.function { color: #dcdcaa; }
.comment { color: #6a9955; }
.boolean { color: #569cd6; }
.private { color: #c586c0; }
#output {
background: #e9ecef;
padding: 15px;
border-radius: 8px;
margin-top: 15px;
min-height: 40px;
font-family: 'Courier New', monospace;
font-size: 0.85rem;
border-left: 4px solid #007bff;
white-space: pre-wrap;
}
table {
width: 100%;
border-collapse: collapse;
margin: 15px 0;
}
th, td {
padding: 10px;
border: 1px solid #ddd;
text-align: left;
}
th { background: #007bff; color: white; }
tr:nth-child(even) { background: #f8f9fa; }
.btn {
padding: 10px 20px;
background: #007bff;
color: white;
border: none;
border-radius: 6px;
font-size: 1em;
font-weight: bold;
cursor: pointer;
margin: 5px;
transition: all 0.3s;
}
.btn:hover {
background: #0056b3;
transform: translateY(-2px);
}
.btn-success { background: #28a745; }
.btn-success:hover { background: #1e7e34; }
.btn-danger { background: #dc3545; }
.btn-danger:hover { background: #a71d2a; }
.balance-display {
font-size: 2.5em;
font-weight: bold;
color: #28a745;
text-align: center;
padding: 20px;
font-family: 'Courier New', monospace;
background: #f8f9fa;
border-radius: 8px;
margin: 15px 0;
}
.input-group {
margin: 10px 0;
}
.input-group label {
display: inline-block;
min-width: 150px;
font-weight: bold;
}
.input-group input {
padding: 8px 12px;
border: 2px solid #ddd;
border-radius: 6px;
font-size: 1em;
width: 150px;
}
.input-group input:focus {
outline: none;
border-color: #007bff;
}
.log-display {
background: #1e1e1e;
color: #d4d4d4;
padding: 15px;
border-radius: 8px;
font-family: 'Courier New', monospace;
font-size: 0.85rem;
max-height: 200px;
overflow-y: auto;
margin: 10px 0;
}
.log-entry { margin: 3px 0; }
.log-success { color: #4ec9b0; }
.log-error { color: #f48771; }
.log-info { color: #569cd6; }
</style>
</head>
<body>
<h1>Encapsulation</h1>
<div class="demo-box">
<h2>1. With Closures</h2>
<pre>
<span class="keyword">function</span> <span class="function">Counter</span>() {
<span class="keyword">let</span> count = <span class="number">0</span>; <span class="comment">// Private!</span>
<span class="keyword">return</span> {
increment: <span class="keyword">function</span> () { count++; },
decrement: <span class="keyword">function</span> () { count--; },
getCount: <span class="keyword">function</span> () { <span class="keyword">return</span> count; }
};
}
<span class="keyword">const</span> counter = <span class="function">Counter</span>();
counter.<span class="function">increment</span>();
counter.<span class="function">increment</span>();
console.log(counter.<span class="function">getCount</span>()); <span class="comment">// 2</span>
console.log(counter.count); <span class="comment">// undefined (private!)</span>
</pre>
</div>
<div class="demo-box">
<h2>2. With ES6 Private Fields (#)</h2>
<pre>
<span class="keyword">class</span> <span class="function">BankAccount</span> {
<span class="private">#balance</span> = <span class="number">0</span>; <span class="comment">// Private field</span>
<span class="function">constructor</span>(initialBalance) {
<span class="keyword">this</span>.<span class="private">#balance</span> = initialBalance;
}
<span class="function">deposit</span>(amount) {
<span class="keyword">if</span> (amount < <span class="number">0</span>) <span class="keyword">throw new</span> <span class="function">Error</span>(<span class="string">"Cannot deposit negative amount"</span>);
<span class="keyword">this</span>.<span class="private">#balance</span> += amount;
}
<span class="function">withdraw</span>(amount) {
<span class="keyword">if</span> (amount > <span class="keyword">this</span>.<span class="private">#balance</span>) <span class="keyword">throw new</span> <span class="function">Error</span>(<span class="string">"Insufficient balance"</span>);
<span class="keyword">this</span>.<span class="private">#balance</span> -= amount;
}
<span class="function">getBalance</span>() {
<span class="keyword">return</span> <span class="keyword">this</span>.<span class="private">#balance</span>;
}
}
</pre>
</div>
<div class="demo-box">
<h2>3. With Getters and Setters</h2>
<pre>
<span class="keyword">class</span> <span class="function">Person</span> {
<span class="private">#name</span>;
<span class="function">constructor</span>(name) {
<span class="keyword">this</span>.<span class="private">#name</span> = name;
}
<span class="keyword">get</span> <span class="function">name</span>() {
<span class="keyword">return</span> <span class="keyword">this</span>.<span class="private">#name</span>;
}
<span class="keyword">set</span> <span class="function">name</span>(newName) {
<span class="keyword">if</span> (newName.length < <span class="number">2</span>) {
<span class="keyword">throw new</span> <span class="function">Error</span>(<span class="string">"Name must be at least 2 characters"</span>);
}
<span class="keyword">this</span>.<span class="private">#name</span> = newName;
}
}
</pre>
</div>
<div class="demo-box">
<h2>4. Comparison of Approaches</h2>
<table>
<tr>
<th>Approach</th>
<th>True Privacy?</th>
<th>Modern?</th>
<th>Best For</th>
</tr>
<tr>
<td><strong>Closures</strong></td>
<td>✅ Yes</td>
<td>✅ Yes</td>
<td>Small utilities, factories</td>
</tr>
<tr>
<td><strong>ES6 # Private</strong></td>
<td>✅ Yes (enforced)</td>
<td>✅ Yes</td>
<td>Modern OOP</td>
</tr>
<tr>
<td><strong>Getters/Setters</strong></td>
<td>⚠️ Partial</td>
<td>✅ Yes</td>
<td>Validation, computed props</td>
</tr>
<tr>
<td><strong>Modules</strong></td>
<td>✅ Yes</td>
<td>✅ Yes</td>
<td>Multi-file apps</td>
</tr>
<tr>
<td><strong>IIFE</strong></td>
<td>✅ Yes</td>
<td>⚠️ Legacy</td>
<td>Pre-ES6 code, libraries</td>
</tr>
</table>
</div>
<div class="demo-box">
<h2>5. Interactive: Bank Account</h2>
<p>A bank account with encapsulated balance — you can only interact through methods.</p>
<div class="balance-display" id="balanceDisplay">$0</div>
<div class="input-group">
<label for="amountInput">Amount:</label>
<input type="number" id="amountInput" value="100" step="50">
</div>
<div style="text-align: center;">
<button class="btn btn-success" onclick="deposit()">Deposit</button>
<button class="btn btn-danger" onclick="withdraw()">Withdraw</button>
<button class="btn" onclick="showBalance()">Check Balance</button>
<button class="btn" onclick="tryDirectAccess()">Try account.#balance</button>
</div>
<div class="log-display" id="logDisplay"></div>
</div>
<div class="demo-box">
<h2>6. Interactive: Counter</h2>
<p>A counter using closures — the count is private.</p>
<div class="balance-display" id="counterDisplay">0</div>
<div style="text-align: center;">
<button class="btn btn-success" onclick="counterIncrement()">+ Increment</button>
<button class="btn btn-danger" onclick="counterDecrement()">− Decrement</button>
<button class="btn" onclick="tryCounterAccess()">Try counter.count</button>
</div>
<div class="log-display" id="counterLog"></div>
</div>
<div class="demo-box">
<h2>7. Live Output — All Examples</h2>
<div id="output">Loading...</div>
</div>
<script>
// ============================================
// Encapsulation — Live Demo
// ============================================
let results = [];
// 1. Closures
results.push('📌 Encapsulation with Closures:\n');
function Counter() {
let count = 0;
return {
increment: function () { count++; },
decrement: function () { count--; },
getCount: function () { return count; }
};
}
const counter = Counter();
counter.increment();
counter.increment();
counter.increment();
results.push(' counter.increment() × 3');
results.push(' counter.getCount() → ' + counter.getCount());
results.push(' counter.count → ' + counter.count + ' (undefined — private!)');
results.push('');
// 2. ES6 Private Fields
results.push('📌 Encapsulation with ES6 Private Fields:\n');
class BankAccount {
#balance = 0;
constructor(initialBalance) {
this.#balance = initialBalance;
}
deposit(amount) {
if (amount < 0) throw new Error("Cannot deposit negative amount");
this.#balance += amount;
return this.#balance;
}
withdraw(amount) {
if (amount > this.#balance) throw new Error("Insufficient balance");
this.#balance -= amount;
return this.#balance;
}
getBalance() {
return this.#balance;
}
}
const account = new BankAccount(1000);
results.push(' new BankAccount(1000)');
results.push(' account.getBalance() → $' + account.getBalance());
account.deposit(500);
results.push(' account.deposit(500) → $' + account.getBalance());
account.withdraw(200);
results.push(' account.withdraw(200) → $' + account.getBalance());
try {
account.withdraw(5000);
} catch (error) {
results.push(' account.withdraw(5000) → ❌ ' + error.message);
}
try {
account.deposit(-100);
} catch (error) {
results.push(' account.deposit(-100) → ❌ ' + error.message);
}
results.push('');
// 3. Getters and Setters
results.push('📌 Encapsulation with Getters/Setters:\n');
class Person {
#name;
constructor(name) {
this.#name = name;
}
get name() {
return this.#name;
}
set name(newName) {
if (newName.length < 2) {
throw new Error("Name must be at least 2 characters");
}
this.#name = newName;
}
}
const person = new Person("Alice");
results.push(' new Person("Alice") → name = "' + person.name + '"');
person.name = "Bob";
results.push(' person.name = "Bob" → name = "' + person.name + '"');
try {
person.name = "A";
} catch (error) {
results.push(' person.name = "A" → ❌ ' + error.message);
}
results.push('');
// 4. Module pattern (simulated)
results.push('📌 Encapsulation with Module Pattern:\n');
const CounterModule = (function () {
let count = 0; // Private
return {
increment: function () { count++; return count; },
decrement: function () { count--; return count; },
getCount: function () { return count; }
};
})();
results.push(' CounterModule.increment() → ' + CounterModule.increment());
results.push(' CounterModule.increment() → ' + CounterModule.increment());
results.push(' CounterModule.getCount() → ' + CounterModule.getCount());
results.push(' CounterModule.count → ' + CounterModule.count + ' (private!)');
results.push('');
// 5. IIFE
results.push('📌 Encapsulation with IIFE:\n');
const CounterIIFE = (function () {
let count = 0;
function increment() { count++; }
function decrement() { count--; }
function getCount() { return count; }
return { increment, decrement, getCount };
})();
CounterIIFE.increment();
CounterIIFE.increment();
CounterIIFE.increment();
results.push(' CounterIIFE.increment() × 3');
results.push(' CounterIIFE.getCount() → ' + CounterIIFE.getCount());
results.push(' CounterIIFE.count → ' + CounterIIFE.count + ' (private!)');
results.push('');
// 6. Practical: Temperature with validation
results.push('📌 Practical: Temperature with Validation:\n');
class Temperature {
#celsius = 0;
constructor(celsius) {
this.celsius = celsius; // Uses setter
}
get celsius() {
return this.#celsius;
}
set celsius(value) {
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;
}
}
const temp = new Temperature(25);
results.push(' new Temperature(25)');
results.push(' temp.celsius → ' + temp.celsius + '°C');
results.push(' temp.fahrenheit → ' + temp.fahrenheit + '°F');
temp.fahrenheit = 100;
results.push(' temp.fahrenheit = 100 → celsius = ' + temp.celsius.toFixed(2) + '°C');
try {
temp.celsius = -300;
} catch (error) {
results.push(' temp.celsius = -300 → ❌ ' + error.message);
}
document.getElementById('output').textContent = results.join('\n');
// ============================================
// Interactive: Bank Account
// ============================================
class InteractiveBank {
#balance = 0;
constructor(initial) {
this.#balance = initial;
}
deposit(amount) {
if (amount < 0) throw new Error("Cannot deposit negative amount");
if (amount === 0) throw new Error("Deposit amount must be positive");
this.#balance += amount;
return this.#balance;
}
withdraw(amount) {
if (amount <= 0) throw new Error("Withdrawal must be positive");
if (amount > this.#balance) throw new Error("Insufficient balance");
this.#balance -= amount;
return this.#balance;
}
getBalance() {
return this.#balance;
}
}
const bankAccount = new InteractiveBank(1000);
function logTo(containerId, message, type = 'info') {
const container = document.getElementById(containerId);
const entry = document.createElement('div');
entry.className = 'log-entry log-' + type;
entry.textContent = '> ' + message;
container.appendChild(entry);
container.scrollTop = container.scrollHeight;
}
function updateBalance() {
document.getElementById('balanceDisplay').textContent =
'$' + bankAccount.getBalance().toLocaleString();
}
function deposit() {
const amount = Number(document.getElementById('amountInput').value);
try {
const balance = bankAccount.deposit(amount);
updateBalance();
logTo('logDisplay', `Deposited $${amount}. New balance: $${balance}`, 'success');
} catch (error) {
logTo('logDisplay', error.message, 'error');
}
}
function withdraw() {
const amount = Number(document.getElementById('amountInput').value);
try {
const balance = bankAccount.withdraw(amount);
updateBalance();
logTo('logDisplay', `Withdrew $${amount}. New balance: $${balance}`, 'success');
} catch (error) {
logTo('logDisplay', error.message, 'error');
}
}
function showBalance() {
const balance = bankAccount.getBalance();
logTo('logDisplay', `Balance: $${balance}`, 'info');
}
function tryDirectAccess() {
logTo('logDisplay', 'Attempting: account.#balance', 'info');
try {
// This will fail at parse time in real code — simulated here
logTo('logDisplay', '❌ SyntaxError: Private field #balance must be declared in an enclosing class', 'error');
} catch (error) {
logTo('logDisplay', error.message, 'error');
}
}
// ============================================
// Interactive: Counter
// ============================================
const interactiveCounter = (function () {
let count = 0;
return {
increment: () => ++count,
decrement: () => --count,
getCount: () => count
};
})();
function updateCounterDisplay() {
document.getElementById('counterDisplay').textContent = interactiveCounter.getCount();
}
function counterIncrement() {
const val = interactiveCounter.increment();
updateCounterDisplay();
logTo('counterLog', `Incremented → ${val}`, 'success');
}
function counterDecrement() {
const val = interactiveCounter.decrement();
updateCounterDisplay();
logTo('counterLog', `Decremented → ${val}`, 'success');
}
function tryCounterAccess() {
logTo('counterLog', 'Attempting: counter.count', 'info');
logTo('counterLog', '→ undefined (count is private via closure!)', 'error');
}
// Initialize
updateBalance();
logTo('logDisplay', 'Bank account initialized with $1000', 'info');
logTo('counterLog', 'Counter initialized with 0', 'info');
</script>
</body>
</html>
Quick Reference
Core Concepts
| Concept | Description |
|---|---|
| Data Hiding | Hide internal state to prevent unintended modifications |
| Access Control | Provide controlled access via methods |
| Abstraction | Expose only necessary behavior |
Why Encapsulation Matters
| Benefit | Description |
|---|---|
| Security | Prevents unauthorized changes |
| Maintainability | Easier to modify and debug |
| Reusability | More modular and reusable |
| Scalability | Reduces dependencies |
Approaches Comparison
| Approach | True Privacy? | Modern? | Best For |
|---|---|---|---|
| Closures | ✅ Yes | ✅ Yes | Small utilities, factories |
| ES6 # Private | ✅ Yes (enforced) | ✅ Yes | Modern OOP |
| Getters/Setters | ⚠️ Partial | ✅ Yes | Validation, computed props |
| Modules | ✅ Yes | ✅ Yes | Multi-file apps |
| IIFE | ✅ Yes | ⚠️ Legacy | Pre-ES6 code, libraries |
Best Practices
✅ Do This:
// Use ES6 private fields for modern OOP
class User {
#password;
constructor(password) {
this.#password = password;
}
checkPassword(input) {
return input === this.#password;
}
}
// Use getters/setters for validation
class Person {
#name;
set name(value) {
if (typeof value !== 'string') throw new Error('Invalid name');
this.#name = value;
}
}
// Use closures for factory functions
const createCounter = () => {
let count = 0;
return {
increment: () => ++count,
getCount: () => count
};
};
// Use modules for large apps
// utils.js
let secret = 'private';
export const getSecret = () => secret;
❌ Don’t Do This:
// Don't use underscores for "privacy"
class User {
constructor() {
this._password = 'secret'; // ⚠️ Convention only — still accessible!
}
}
user._password; // Works! Not truly private
// Don't expose internal state directly
class BadCounter {
constructor() {
this.count = 0; // ❌ Public — anyone can change!
}
}
counter.count = -999; // Broken!
// Don't skip validation in setters
class BadPerson {
set age(value) {
this._age = value; // ❌ No validation!
}
}
person.age = -5; // Invalid age accepted!
// Don't confuse getters with methods
class BadExample {
getValue() { return this.#value; } // Method
get value() { return this.#value; } // Getter
}
Common Pitfalls
| Pitfall | Problem | Solution |
|---|---|---|
_underscore convention | Not truly private | Use # or closures |
| Forgetting validation | Invalid state | Validate in setters |
| Exposing references | Internal objects mutated | Return copies |
| Over-encapsulating | Too many getters | Expose meaningful operations |
# outside class | SyntaxError | Only inside class body |
Real-World Examples
// 1. React component with private state (via hooks)
function Counter() {
const [count, setCount] = useState(0); // Encapsulated
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
// 2. Node.js module
// db.js
let connection; // Private
export function connect() { connection = createConnection(); }
export function query(sql) { return connection.query(sql); }
// 3. Service class
class ApiService {
#baseUrl;
#token;
constructor(baseUrl, token) {
this.#baseUrl = baseUrl;
this.#token = token;
}
async get(endpoint) {
const response = await fetch(`${this.#baseUrl}${endpoint}`, {
headers: { Authorization: `Bearer ${this.#token}` }
});
return response.json();
}
}
Pro Tip: Encapsulation is about protecting your data and hiding implementation details. Use ES6 private fields (#) for modern OOP — they’re enforced by the language. Use closures for factory functions and small modules. Use getters/setters when you need validation or computed properties. Use modules for multi-file applications. Remember: the _underscore convention is just a convention — it doesn’t provide true privacy. For real encapsulation, use # private fields (modern) or closures (universal)!
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!