封装性:保护数据和隐藏实现细节
为什么需要封装
在日常生活中, 我们使用各种设备时很少需要了解它们内部的工作原理。使用智能手机时, 我们只关心触摸屏幕、点击图标这些简单操作, 而不需要知道处理器如何执行指令、内存如何分配。这种"隐藏复杂性, 暴露简单接口"的理念, 就是封装的核心思想。
封装(Encapsulation)是面向对象编程的基本原则之一, 它有两个主要目标:
- 数据隐藏: 将对象的内部状态隐藏起来, 防止外部直接访问和修改
- 接口简化: 只暴露必要的公共方法, 隐藏复杂的实现细节
让我们通过一个实际例子理解封装的重要性:
javascript
// 没有封装的代码 - 问题示例
const userAccount = {
balance: 1000,
overdraftLimit: 500,
};
// 任何代码都可以直接修改余额
userAccount.balance = 999999; // 😱 没有验证!
userAccount.overdraftLimit = -1000; // 😱 无效的值!
// 甚至可以添加奇怪的属性
userAccount.secretMoney = 1000000; // 😱 数据结构被破坏!这种方式存在严重问题:没有任何保护机制, 数据可以被任意修改, 容易导致错误状态。
javascript
// 使用封装的代码 - 解决方案
class UserAccount {
// 私有字段 - 外部无法访问
#balance;
#overdraftLimit;
constructor(initialBalance, overdraftLimit = 0) {
if (initialBalance < 0) {
throw new Error("初始余额不能为负数");
}
if (overdraftLimit < 0) {
throw new Error("透支额度不能为负数");
}
this.#balance = initialBalance;
this.#overdraftLimit = overdraftLimit;
}
// 公共接口 - 受控的访问
deposit(amount) {
if (amount <= 0) {
throw new Error("存款金额必须大于0");
}
this.#balance += amount;
return this.#balance;
}
withdraw(amount) {
const maxWithdraw = this.#balance + this.#overdraftLimit;
if (amount <= 0) {
throw new Error("取款金额必须大于0");
}
if (amount > maxWithdraw) {
throw new Error(`余额不足, 最多可取 $${maxWithdraw}`);
}
this.#balance -= amount;
return this.#balance;
}
getBalance() {
return this.#balance;
}
}
const account = new UserAccount(1000, 500);
// 只能通过公共方法操作
account.deposit(200); // ✓ 安全
console.log(account.getBalance()); // 1200
// 尝试非法操作会被阻止
try {
account.withdraw(2000); // 超出限制
} catch (error) {
console.log(error.message); // "余额不足, 最多可取 $1700"
}
// 无法直接修改私有字段
console.log(account.#balance); // SyntaxError
account.#balance = 999999; // SyntaxErrorJavaScript 中实现封装的方法
1. 私有字段(Class Private Fields)
ES2022 引入了真正的私有字段, 使用 # 前缀:
javascript
class SmartDevice {
// 私有字段
#deviceId;
#encryptionKey;
#isLocked;
// 公共字段
name;
model;
constructor(name, model) {
this.name = name;
this.model = model;
this.#deviceId = this.#generateId();
this.#encryptionKey = this.#generateKey();
this.#isLocked = true;
}
// 私有方法
#generateId() {
return `DEVICE-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
#generateKey() {
return Math.random().toString(36).substr(2, 15);
}
#encrypt(data) {
// 简化的加密逻辑
return `encrypted:${data}:${this.#encryptionKey}`;
}
#decrypt(encryptedData) {
// 简化的解密逻辑
const parts = encryptedData.split(":");
return parts[1];
}
// 公共方法
unlock(password) {
if (password === "correct-password") {
// 实际应用中应该更复杂
this.#isLocked = false;
console.log(`${this.name} 已解锁`);
return true;
}
console.log("密码错误");
return false;
}
lock() {
this.#isLocked = true;
console.log(`${this.name} 已锁定`);
}
storeData(key, value) {
if (this.#isLocked) {
throw new Error("设备已锁定, 无法存储数据");
}
const encryptedValue = this.#encrypt(value);
console.log(`数据已加密存储: ${key}`);
return encryptedValue;
}
getDeviceInfo() {
// 只暴露部分信息
return {
name: this.name,
model: this.model,
isLocked: this.#isLocked,
// deviceId 和 encryptionKey 保持私有
};
}
}
const phone = new SmartDevice("iPhone", "15 Pro");
console.log(phone.getDeviceInfo());
// { name: 'iPhone', model: '15 Pro', isLocked: true }
phone.unlock("correct-password");
const encrypted = phone.storeData("password", "mySecret123");
// 私有字段和方法无法从外部访问
console.log(phone.#deviceId); // SyntaxError
phone.#encrypt("data"); // SyntaxError2. WeakMap 实现私有数据
在 ES2022 之前, 可以使用 WeakMap 模拟私有数据:
javascript
const privateData = new WeakMap();
class SecureWallet {
constructor(owner, initialBalance) {
// 将私有数据存储在 WeakMap 中
privateData.set(this, {
owner: owner,
balance: initialBalance,
pin: this.#generatePin(),
transactions: [],
});
}
#generatePin() {
return Math.floor(1000 + Math.random() * 9000);
}
#getData() {
return privateData.get(this);
}
#recordTransaction(type, amount) {
const data = this.#getData();
data.transactions.push({
type,
amount,
balance: data.balance,
timestamp: new Date(),
});
}
verifyPin(pin) {
const data = this.#getData();
return pin === data.pin;
}
deposit(amount, pin) {
if (!this.verifyPin(pin)) {
throw new Error("PIN码错误");
}
if (amount <= 0) {
throw new Error("存款金额必须大于0");
}
const data = this.#getData();
data.balance += amount;
this.#recordTransaction("DEPOSIT", amount);
return `存款成功, 当前余额: $${data.balance}`;
}
withdraw(amount, pin) {
if (!this.verifyPin(pin)) {
throw new Error("PIN码错误");
}
const data = this.#getData();
if (amount > data.balance) {
throw new Error("余额不足");
}
data.balance -= amount;
this.#recordTransaction("WITHDRAW", amount);
return `取款成功, 当前余额: $${data.balance}`;
}
getBalance(pin) {
if (!this.verifyPin(pin)) {
throw new Error("PIN码错误");
}
return this.#getData().balance;
}
getTransactionHistory(pin) {
if (!this.verifyPin(pin)) {
throw new Error("PIN码错误");
}
return [...this.#getData().transactions];
}
}
const wallet = new SecureWallet("Sarah", 1000);
// 即使查看对象, 也看不到私有数据
console.log(wallet);
// SecureWallet {}
// 必须使用正确的 PIN 才能操作
const correctPin = 1234; // 实际应用中通过安全方式获取
try {
wallet.deposit(500, 9999); // 错误的 PIN
} catch (error) {
console.log(error.message); // "PIN码错误"
}3. 闭包实现封装
使用闭包创建真正的私有变量:
javascript
function createBankAccount(accountHolder, initialDeposit) {
// 这些变量是真正私有的, 只能通过返回的方法访问
let balance = initialDeposit;
let accountNumber = generateAccountNumber();
let transactionLog = [];
let isActive = true;
function generateAccountNumber() {
return `ACC-${Date.now()}-${Math.random()
.toString(36)
.substr(2, 6)
.toUpperCase()}`;
}
function validateAmount(amount) {
if (typeof amount !== "number" || amount <= 0) {
throw new Error("金额必须是正数");
}
}
function log(type, amount, success) {
transactionLog.push({
type,
amount,
success,
balance: balance,
timestamp: new Date(),
});
}
// 返回公共接口
return {
deposit(amount) {
if (!isActive) {
throw new Error("账户已冻结");
}
try {
validateAmount(amount);
balance += amount;
log("DEPOSIT", amount, true);
return {
success: true,
newBalance: balance,
message: `成功存入 $${amount}`,
};
} catch (error) {
log("DEPOSIT", amount, false);
throw error;
}
},
withdraw(amount) {
if (!isActive) {
throw new Error("账户已冻结");
}
try {
validateAmount(amount);
if (amount > balance) {
throw new Error(`余额不足, 当前余额 $${balance}`);
}
balance -= amount;
log("WITHDRAW", amount, true);
return {
success: true,
newBalance: balance,
message: `成功取出 $${amount}`,
};
} catch (error) {
log("WITHDRAW", amount, false);
throw error;
}
},
getBalance() {
return balance;
},
getAccountInfo() {
return {
accountHolder,
accountNumber,
balance,
isActive,
totalTransactions: transactionLog.length,
};
},
getStatement(limit = 10) {
return {
accountNumber,
recentTransactions: transactionLog.slice(-limit),
};
},
freezeAccount() {
isActive = false;
console.log("账户已冻结");
},
unfreezeAccount() {
isActive = true;
console.log("账户已解冻");
},
};
}
const myAccount = createBankAccount("John Doe", 5000);
console.log(myAccount.deposit(1000));
// { success: true, newBalance: 6000, message: '成功存入 $1000' }
console.log(myAccount.withdraw(500));
// { success: true, newBalance: 5500, message: '成功取出 $500' }
console.log(myAccount.getAccountInfo());
// {
// accountHolder: 'John Doe',
// accountNumber: 'ACC-...',
// balance: 5500,
// isActive: true,
// totalTransactions: 2
// }
// 无法直接访问私有变量
console.log(myAccount.balance); // undefined
console.log(myAccount.accountNumber); // undefined
console.log(myAccount.transactionLog); // undefined访问器属性(Getters 和 Setters)
Getter 和 Setter 提供了对属性的受控访问:
javascript
class Temperature {
#celsius;
constructor(celsius = 0) {
this.#celsius = celsius;
}
// Getter - 获取摄氏度
get celsius() {
return this.#celsius;
}
// Setter - 设置摄氏度
set celsius(value) {
if (typeof value !== "number") {
throw new Error("温度必须是数字");
}
if (value < -273.15) {
throw new Error("温度不能低于绝对零度 (-273.15°C)");
}
this.#celsius = value;
}
// Getter - 获取华氏度
get fahrenheit() {
return (this.#celsius * 9) / 5 + 32;
}
// Setter - 通过华氏度设置温度
set fahrenheit(value) {
if (typeof value !== "number") {
throw new Error("温度必须是数字");
}
this.celsius = ((value - 32) * 5) / 9; // 使用 celsius setter 进行验证
}
// Getter - 获取开尔文温度
get kelvin() {
return this.#celsius + 273.15;
}
// Setter - 通过开尔文温度设置
set kelvin(value) {
this.celsius = value - 273.15;
}
toString() {
return `${this.#celsius.toFixed(2)}°C = ${this.fahrenheit.toFixed(
2
)}°F = ${this.kelvin.toFixed(2)}K`;
}
}
const temp = new Temperature(25);
// 使用 getter 像访问属性一样
console.log(temp.celsius); // 25
console.log(temp.fahrenheit); // 77
console.log(temp.kelvin); // 298.15
// 使用 setter 像设置属性一样, 但有验证
temp.celsius = 100;
console.log(temp.toString());
// 100.00°C = 212.00°F = 373.15K
temp.fahrenheit = 32; // 设置为冰点
console.log(temp.celsius); // 0
// 错误处理
try {
temp.celsius = -300; // 低于绝对零度
} catch (error) {
console.log(error.message);
// "温度不能低于绝对零度 (-273.15°C)"
}计算属性和验证
javascript
class Rectangle {
#width;
#height;
constructor(width, height) {
this.width = width; // 使用 setter
this.height = height; // 使用 setter
}
get width() {
return this.#width;
}
set width(value) {
if (value <= 0) {
throw new Error("宽度必须大于0");
}
this.#width = value;
}
get height() {
return this.#height;
}
set height(value) {
if (value <= 0) {
throw new Error("高度必须大于0");
}
this.#height = value;
}
// 计算属性 - 只有 getter
get area() {
return this.#width * this.#height;
}
get perimeter() {
return 2 * (this.#width + this.#height);
}
get diagonal() {
return Math.sqrt(this.#width ** 2 + this.#height ** 2);
}
get aspectRatio() {
const gcd = (a, b) => (b === 0 ? a : gcd(b, a % b));
const divisor = gcd(this.#width, this.#height);
return `${this.#width / divisor}:${this.#height / divisor}`;
}
// 可以设置面积, 但会保持宽高比
set area(newArea) {
const currentArea = this.area;
const scale = Math.sqrt(newArea / currentArea);
this.#width *= scale;
this.#height *= scale;
}
toString() {
return `Rectangle(${this.#width} × ${this.#height})
Area: ${this.area}
Perimeter: ${this.perimeter.toFixed(2)}
Diagonal: ${this.diagonal.toFixed(2)}
Aspect Ratio: ${this.aspectRatio}`;
}
}
const rect = new Rectangle(16, 9);
console.log(rect.area); // 144
console.log(rect.perimeter); // 50
console.log(rect.aspectRatio); // "16:9"
// 修改尺寸
rect.width = 20;
rect.height = 10;
console.log(rect.area); // 200
// 通过设置面积来缩放
rect.area = 100;
console.log(rect.width, rect.height);
// 两个值都按比例缩放
console.log(rect.toString());封装的设计原则
1. 最小知识原则(Principle of Least Knowledge)
对象应该只暴露必要的接口, 隐藏内部实现:
javascript
class EmailService {
#apiKey;
#apiEndpoint;
#rateLimiter;
constructor(apiKey) {
this.#apiKey = apiKey;
this.#apiEndpoint = "https://api.emailservice.com";
this.#rateLimiter = new RateLimiter(100); // 每分钟100封
}
// 私有方法 - 内部实现细节
#buildHeaders() {
return {
Authorization: `Bearer ${this.#apiKey}`,
"Content-Type": "application/json",
};
}
#validateEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
throw new Error(`无效的邮箱地址: ${email}`);
}
}
#checkRateLimit() {
if (!this.#rateLimiter.canSend()) {
throw new Error("发送频率超限, 请稍后再试");
}
}
async #makeRequest(endpoint, data) {
const response = await fetch(`${this.#apiEndpoint}${endpoint}`, {
method: "POST",
headers: this.#buildHeaders(),
body: JSON.stringify(data),
});
if (!response.ok) {
throw new Error(`API 错误: ${response.status}`);
}
return await response.json();
}
// 公共接口 - 简单易用
async sendEmail(to, subject, body) {
this.#validateEmail(to);
this.#checkRateLimit();
const result = await this.#makeRequest("/send", {
to,
subject,
body,
timestamp: new Date().toISOString(),
});
this.#rateLimiter.recordSent();
return {
success: true,
messageId: result.id,
to,
subject,
};
}
async sendBulkEmails(recipients) {
const results = [];
for (const recipient of recipients) {
try {
const result = await this.sendEmail(
recipient.email,
recipient.subject,
recipient.body
);
results.push({ ...result, status: "sent" });
} catch (error) {
results.push({
to: recipient.email,
status: "failed",
error: error.message,
});
}
}
return {
total: recipients.length,
sent: results.filter((r) => r.status === "sent").length,
failed: results.filter((r) => r.status === "failed").length,
results,
};
}
}
class RateLimiter {
#maxPerMinute;
#sentCount;
#resetTime;
constructor(maxPerMinute) {
this.#maxPerMinute = maxPerMinute;
this.#sentCount = 0;
this.#resetTime = Date.now() + 60000;
}
canSend() {
this.#checkReset();
return this.#sentCount < this.#maxPerMinute;
}
recordSent() {
this.#checkReset();
this.#sentCount++;
}
#checkReset() {
if (Date.now() >= this.#resetTime) {
this.#sentCount = 0;
this.#resetTime = Date.now() + 60000;
}
}
}
// 使用 - 简单明了
const emailService = new EmailService("api-key-123");
// 用户只需要知道如何发送邮件, 不需要了解:
// - API 密钥如何存储
// - 请求头如何构建
// - 速率限制如何实现
// - API 端点的具体地址
await emailService.sendEmail(
"[email protected]",
"Welcome!",
"Thanks for joining!"
);2. 单一职责原则
每个类应该只有一个改变的理由:
javascript
// 好的设计 - 职责分离
class User {
#id;
#name;
#email;
#passwordHash;
constructor(id, name, email) {
this.#id = id;
this.#name = name;
this.#email = email;
}
setPassword(hashedPassword) {
this.#passwordHash = hashedPassword;
}
verifyPassword(hashedPassword) {
return this.#passwordHash === hashedPassword;
}
updateEmail(newEmail) {
// 验证邮箱格式
if (!this.#isValidEmail(newEmail)) {
throw new Error("无效的邮箱地址");
}
this.#email = newEmail;
}
#isValidEmail(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
toJSON() {
return {
id: this.#id,
name: this.#name,
email: this.#email,
};
}
}
// 密码哈希化 - 独立的类
class PasswordHasher {
#salt;
constructor() {
this.#salt = this.#generateSalt();
}
#generateSalt() {
return Math.random().toString(36).substring(2);
}
hash(password) {
// 简化的哈希逻辑
return `${password}${this.#salt}`
.split("")
.reduce((hash, char) => (hash << 5) - hash + char.charCodeAt(0), 0)
.toString(36);
}
verify(password, hash) {
return this.hash(password) === hash;
}
}
// 用户持久化 - 独立的类
class UserRepository {
#users;
constructor() {
this.#users = new Map();
}
save(user) {
const data = user.toJSON();
this.#users.set(data.id, data);
return data.id;
}
findById(id) {
return this.#users.get(id) || null;
}
findByEmail(email) {
for (const user of this.#users.values()) {
if (user.email === email) {
return user;
}
}
return null;
}
delete(id) {
return this.#users.delete(id);
}
}
// 用户认证服务 - 协调各个组件
class AuthService {
#userRepository;
#passwordHasher;
constructor(userRepository, passwordHasher) {
this.#userRepository = userRepository;
this.#passwordHasher = passwordHasher;
}
register(name, email, password) {
// 检查邮箱是否已存在
if (this.#userRepository.findByEmail(email)) {
throw new Error("邮箱已被注册");
}
// 创建用户
const user = new User(Date.now(), name, email);
// 哈希密码
const hashedPassword = this.#passwordHasher.hash(password);
user.setPassword(hashedPassword);
// 保存用户
this.#userRepository.save(user);
return { success: true, message: "注册成功" };
}
login(email, password) {
// 查找用户
const userData = this.#userRepository.findByEmail(email);
if (!userData) {
return { success: false, message: "用户不存在" };
}
// 验证密码
const user = new User(userData.id, userData.name, userData.email);
// 注意:实际应用中需要从数据库读取密码哈希
return {
success: true,
message: "登录成功",
user: userData,
};
}
}
// 使用
const userRepo = new UserRepository();
const hasher = new PasswordHasher();
const authService = new AuthService(userRepo, hasher);
authService.register("Alice", "[email protected]", "password123");
const result = authService.login("[email protected]", "password123");
console.log(result);封装的实际应用
缓存管理器
javascript
class CacheManager {
#cache;
#maxSize;
#ttl; // time to live in milliseconds
constructor(maxSize = 100, ttl = 3600000) {
// 默认1小时
this.#cache = new Map();
this.#maxSize = maxSize;
this.#ttl = ttl;
}
#isExpired(entry) {
return Date.now() - entry.timestamp > this.#ttl;
}
#evictOldest() {
if (this.#cache.size >= this.#maxSize) {
const oldestKey = this.#cache.keys().next().value;
this.#cache.delete(oldestKey);
}
}
#cleanExpired() {
for (const [key, entry] of this.#cache.entries()) {
if (this.#isExpired(entry)) {
this.#cache.delete(key);
}
}
}
set(key, value) {
this.#cleanExpired();
this.#evictOldest();
this.#cache.set(key, {
value,
timestamp: Date.now(),
hits: 0,
});
}
get(key) {
const entry = this.#cache.get(key);
if (!entry) {
return null;
}
if (this.#isExpired(entry)) {
this.#cache.delete(key);
return null;
}
entry.hits++;
entry.lastAccess = Date.now();
return entry.value;
}
has(key) {
return this.get(key) !== null;
}
delete(key) {
return this.#cache.delete(key);
}
clear() {
this.#cache.clear();
}
getStats() {
this.#cleanExpired();
let totalHits = 0;
let avgAge = 0;
const now = Date.now();
for (const entry of this.#cache.values()) {
totalHits += entry.hits;
avgAge += now - entry.timestamp;
}
return {
size: this.#cache.size,
maxSize: this.#maxSize,
totalHits,
avgHits: totalHits / this.#cache.size || 0,
avgAge: avgAge / this.#cache.size || 0,
};
}
}
const cache = new CacheManager(50, 60000); // 50项, 1分钟TTL
cache.set("user:1", { name: "John", email: "[email protected]" });
cache.set("user:2", { name: "Sarah", email: "[email protected]" });
console.log(cache.get("user:1"));
// { name: 'John', email: '[email protected]' }
console.log(cache.getStats());
// { size: 2, maxSize: 50, totalHits: 1, ... }封装的最佳实践
- 默认私有: 除非确实需要公开, 否则所有成员都应该是私有的
- 清晰的接口: 公共方法应该有清晰的命名和文档
- 验证输入: 公共方法应该验证所有输入
- 返回副本: 当返回内部对象时, 返回副本而不是引用
- 不变性: 考虑使对象的某些部分不可变
小结
封装是面向对象编程的核心原则, 它通过:
- 数据隐藏: 保护对象的内部状态
- 接口简化: 只暴露必要的操作
- 实现隐藏: 允许改变内部实现而不影响外部代码
正确使用封装可以:
- 提高代码的安全性和健壮性
- 降低系统的复杂度
- 提高代码的可维护性和可扩展性
- 防止不恰当的使用