理解封装在面向对象编程中的核心作用
掌握使用闭包实现真正私有成员的方法
了解基于 ES6 类的封装实践及其局限性
能够根据项目需求选择合适的封装策略
封装是面向对象编程的三大基本特性之一。它的核心思想是:
隐藏对象的内部实现细节,仅暴露必要的接口供外部使用。
通过封装,我们可以:
保护数据不被非法修改
控制对内部状态的访问方式
降低模块间的耦合度
提高代码的可维护性和安全性
在 JavaScript 中,由于语言本身的动态特性,实现封装需要借助特定技巧。
JavaScript 的函数作用域和闭包机制可以创建真正的私有变量和方法,这是最可靠的封装方式。
闭包版
function BankAccount(accountNumber, accountHolderName, balance) {
// 私有变量(外部无法直接访问)
let _accountNumber = accountNumber;
let _accountHolderName = accountHolderName;
let _balance = balance;
// 私有方法
function showAccountDetails() {
console.log(`Account Number: ${_accountNumber}`);
console.log(`Account Holder Name: ${_accountHolderName}`);
console.log(`Balance: ${_balance}`);
}
// 公共方法(通过返回对象暴露)
function deposit(amount) {
if (amount <= 0) {
console.log("Deposit amount must be positive");
return;
}
_balance += amount;
showAccountDetails();
}
function withdraw(amount) {
if (_balance >= amount && amount > 0) {
_balance -= amount;
showAccountDetails();
} else {
console.log("Insufficient Balance or invalid amount");
}
}
// 返回公共接口
return {
deposit: deposit,
withdraw: withdraw
};
}
// 使用示例
const myAccount = BankAccount("123456", "John Doe", 1000);
myAccount.deposit(500); // 成功存款
myAccount.withdraw(2000); // 余额不足
// 尝试直接访问私有变量(失败)
console.log(myAccount._balance); // undefined真正的私有性:外部无法通过任何方式直接访问 _balance 等变量
数据完整性:所有状态变更都经过验证逻辑
内存开销:每个实例都会创建独立的函数副本(无法共享方法)
无法扩展:返回的对象是普通对象,不能通过原型链扩展
ES6 的 class 语法提供了更结构化的面向对象编程方式,但默认不支持真正的私有成员。
class BankAccount {
constructor(accountNumber, accountHolderName, balance) {
// 使用下划线前缀表示“私有”(约定俗成)
this._accountNumber = accountNumber;
this._accountHolderName = accountHolderName;
this._balance = balance;
}
// 公共方法
showAccountDetails() {
console.log(`Account Number: ${this._accountNumber}`);
console.log(`Account Holder Name: ${this._accountHolderName}`);
console.log(`Balance: ${this._balance}`);
}
deposit(amount) {
if (amount <= 0) {
console.log("Deposit amount must be positive");
return;
}
this._balance += amount;
this.showAccountDetails();
}
withdraw(amount) {
if (this._balance >= amount && amount > 0) {
this._balance -= amount;
this.showAccountDetails();
} else {
console.log("Insufficient Balance or invalid amount");
}
}
}
// 使用示例
const account = new BankAccount("123456", "John Doe", 1000);
account.deposit(500);
// 技术上仍可访问“私有”属性(不推荐)
console.log(account._balance); // 1500(但违反了封装原则)只是约定:下划线前缀 _ 仅是开发者的约定,技术上仍可被外部访问和修改
方法共享:所有实例共享原型上的方法,内存效率高
可扩展性强:支持继承、原型链扩展等高级特性
现代化 JavaScript ES2022+
从 ECMAScript 2022 开始,JavaScript 原生支持私有字段,使用 # 前缀声明。
class BankAccount {
// 私有字段(真正的私有)
#accountNumber;
#accountHolderName;
#balance;
constructor(accountNumber, accountHolderName, balance) {
this.#accountNumber = accountNumber;
this.#accountHolderName = accountHolderName;
this.#balance = balance;
}
// 私有方法
#showAccountDetails() {
console.log(`Account Number: ${this.#accountNumber}`);
console.log(`Account Holder Name: ${this.#accountHolderName}`);
console.log(`Balance: ${this.#balance}`);
}
deposit(amount) {
if (amount <= 0) {
console.log("Deposit amount must be positive");
return;
}
this.#balance += amount;
this.#showAccountDetails();
}
withdraw(amount) {
if (this.#balance >= amount && amount > 0) {
this.#balance -= amount;
this.#showAccountDetails();
} else {
console.log("Insufficient Balance or invalid amount");
}
}
// 提供受控的只读访问(可选)
getBalance() {
return this.#balance;
}
}
const account = new BankAccount("123456", "John Doe", 1000);
account.deposit(500);
// 尝试访问私有字段(报错!)
// console.log(account.#balance); // SyntaxError: Private field '#balance' must be declared in an enclosing class真正的私有性:语法层面禁止外部访问
运行时错误:试图访问会抛出明确的语法错误
与类系统完美集成:支持私有方法、私有 getter/setter 等
注意:需要现代浏览器或构建工具(如 Babel)支持 ES2022+。
新项目优先使用私有字段(#):提供最强的封装保证
兼容性要求高时使用闭包:当需要支持旧浏览器且必须保证私有性
团队协作项目可使用下划线约定:配合 ESLint 规则(如 no-underscore-dangle)约束访问
始终提供清晰的公共接口:无论采用哪种方式,都要设计合理的 API
封装的核心目标是控制访问权限,保护对象内部状态
闭包提供最可靠的私有性,但牺牲了内存效率和可扩展性
ES6 类提供了结构化语法,但传统写法仅能实现“伪私有”
私有字段(#) 是现代 JavaScript 的标准解决方案,兼顾安全性与性能
选择封装方式时需权衡:安全性、性能、兼容性、可维护性
在闭包实现的 BankAccount 中,为什么每次创建账户实例都会产生新的 deposit 和 withdraw 函数?这会带来什么性能影响?
如果使用类的下划线约定方式,如何通过工具(如 ESLint)防止开发者意外访问私有属性?
私有字段(#balance)和 Symbol 实现的“伪私有”属性有何本质区别?为什么 Symbol 不能真正实现封装?