Skip to content

封装性:保护数据和隐藏实现细节

为什么需要封装

在日常生活中, 我们使用各种设备时很少需要了解它们内部的工作原理。使用智能手机时, 我们只关心触摸屏幕、点击图标这些简单操作, 而不需要知道处理器如何执行指令、内存如何分配。这种"隐藏复杂性, 暴露简单接口"的理念, 就是封装的核心思想。

封装(Encapsulation)是面向对象编程的基本原则之一, 它有两个主要目标:

  1. 数据隐藏: 将对象的内部状态隐藏起来, 防止外部直接访问和修改
  2. 接口简化: 只暴露必要的公共方法, 隐藏复杂的实现细节

让我们通过一个实际例子理解封装的重要性:

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; // SyntaxError

JavaScript 中实现封装的方法

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"); // SyntaxError

2. 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,  ... }

封装的最佳实践

  1. 默认私有: 除非确实需要公开, 否则所有成员都应该是私有的
  2. 清晰的接口: 公共方法应该有清晰的命名和文档
  3. 验证输入: 公共方法应该验证所有输入
  4. 返回副本: 当返回内部对象时, 返回副本而不是引用
  5. 不变性: 考虑使对象的某些部分不可变

小结

封装是面向对象编程的核心原则, 它通过:

  • 数据隐藏: 保护对象的内部状态
  • 接口简化: 只暴露必要的操作
  • 实现隐藏: 允许改变内部实现而不影响外部代码

正确使用封装可以:

  • 提高代码的安全性和健壮性
  • 降低系统的复杂度
  • 提高代码的可维护性和可扩展性
  • 防止不恰当的使用