大家好,我是今天的主讲人,咱们聊聊 JavaScript 里那些“藏起来的小秘密”——也就是 ES2022 引入的 Private Methods
和 Private Accessors
。它们可是提升类封装性的利器!准备好了吗?咱们开始!
开场白:封装,封装,还是封装!
在面向对象编程的世界里,封装绝对是核心概念之一。它就像给你的代码穿上盔甲,保护内部数据不被外部随意篡改,让代码更加健壮,更容易维护。
想象一下,你有一辆汽车。你可以开车、加油、换挡,但你不能直接控制发动机内部的每一个零件。那是因为汽车制造商对发动机进行了封装,只暴露了必要的接口给你。
JavaScript 之前的版本,虽然也能模拟私有属性和方法,但总感觉差点意思。ES2022 带来的 Private Methods
和 Private Accessors
,才是真正的“原生”私有,让封装变得更加可靠。
第一幕:老朋友的“伪装术”——之前的私有模拟
在 ES2022 之前,JavaScript 社区流行几种模拟私有的方法,但它们都有各自的局限性。
-
命名约定(约定成俗,但防君子不防小人)
最常见的就是使用下划线
_
开头来表示私有属性或方法。class Counter { constructor() { this._count = 0; // 下划线表示私有,但外部仍然可以访问 } _increment() { // 下划线表示私有,但外部仍然可以调用 this._count++; } getCount() { return this._count; } } const counter = new Counter(); counter._count = 100; // 仍然可以修改! console.log(counter.getCount()); // 输出 100
这种方式完全依赖开发者的自觉性,只能算是一种“君子协定”。只要你想,随时都可以访问和修改。
-
闭包(稍微靠谱,但略显笨重)
利用闭包的特性,将私有变量和方法放在函数内部,然后返回一个包含公有方法的对象。
function createCounter() { let count = 0; // 私有变量 return { increment() { count++; }, getCount() { return count; } }; } const counter = createCounter(); counter.count = 100; // 无法修改私有变量 count counter.increment(); console.log(counter.getCount()); // 输出 1
这种方式确实可以实现私有,但代码结构会变得复杂,而且每次创建实例都要创建一个新的闭包,略显笨重。
-
WeakMap(性能尚可,但语法稍显繁琐)
使用
WeakMap
来存储私有变量,将实例作为键,私有变量作为值。const _count = new WeakMap(); class Counter { constructor() { _count.set(this, 0); } increment() { _count.set(this, _count.get(this) + 1); } getCount() { return _count.get(this); } } const counter = new Counter(); // 无法直接访问私有变量 counter.increment(); console.log(counter.getCount()); // 输出 1
WeakMap
相对闭包性能更好,但语法也比较繁琐,需要单独创建一个WeakMap
对象,并在每次访问私有变量时都要使用get
和set
方法。
第二幕:ES2022 的“真·私有”登场!
ES2022 引入了使用 #
前缀来声明私有属性和方法。这是一种真正的、原生的私有,JavaScript 引擎会强制限制外部访问。
-
Private Fields(私有字段)
class Counter { #count = 0; // 私有字段 increment() { this.#count++; } getCount() { return this.#count; } } const counter = new Counter(); //console.log(counter.#count); // 报错:Private field '#count' must be declared in an enclosing class counter.increment(); console.log(counter.getCount()); // 输出 1 class SubCounter extends Counter { // incrementSub() { // this.#count++; // 报错:Private field '#count' must be declared in an enclosing class // } }
注意几点:
- 私有字段必须在类中声明,不能在构造函数外部动态添加。
- 只能在类内部访问私有字段,包括方法和访问器。
- 子类无法访问父类的私有字段。这是为了保证封装的彻底性。
-
Private Methods(私有方法)
class Calculator { #add(x, y) { // 私有方法 return x + y; } calculate(x, y) { return this.#add(x, y); } } const calculator = new Calculator(); //console.log(calculator.#add(1, 2)); // 报错:Private field '#add' must be declared in an enclosing class console.log(calculator.calculate(1, 2)); // 输出 3
私有方法只能在类内部调用,外部无法访问。
-
Private Accessors(私有访问器)
私有访问器允许你控制对私有字段的读取和设置,就像
getter
和setter
一样,只不过是私有的。class Temperature { #celsius = 0; get #fahrenheit() { // 私有 getter return (this.#celsius * 9) / 5 + 32; } set #fahrenheit(value) { // 私有 setter this.#celsius = ((value - 32) * 5) / 9; } getCelsius() { return this.#celsius; } setFahrenheit(value) { this.#fahrenheit = value; // 通过私有 setter 设置摄氏度 } getFahrenheit() { return this.#fahrenheit; // 通过私有 getter 获取华氏度 } } const temp = new Temperature(); temp.setFahrenheit(68); console.log(temp.getCelsius()); // 输出 20 console.log(temp.getFahrenheit()); // 输出 68
私有访问器可以让你在读取和设置私有字段时执行一些额外的逻辑,比如验证输入、触发事件等等。
第三幕:in
操作符的妙用
in
操作符可以用来检查一个对象是否拥有某个属性。对于私有字段,in
操作符只能在类内部使用。
class MyClass {
#privateField;
hasPrivateField() {
return #privateField in this; // 返回 true
}
static hasPrivateField(obj) {
return #privateField in obj; // 报错:Private field '#privateField' must be declared in an enclosing class
}
}
const myObj = new MyClass();
console.log(myObj.hasPrivateField()); // 输出 true
//console.log(MyClass.hasPrivateField(myObj)); // 报错
in
操作符对于检查私有字段的存在性非常有用,特别是在处理复杂的逻辑时。
第四幕:为什么要用“真·私有”?
- 更强的封装性: 真正的私有,防止外部随意访问和修改内部数据,保证类的完整性和一致性。
- 更好的代码可维护性: 修改内部实现不会影响外部代码,降低了重构的风险。
- 更安全的代码: 避免恶意代码通过访问私有属性来破坏程序的行为。
- 避免命名冲突: 不再需要担心私有属性的命名与其他属性冲突。
第五幕:表格对比:新旧私有方案大PK
特性 | 命名约定(_ ) |
闭包 | WeakMap | Private Fields/Methods/Accessors(# ) |
---|---|---|---|---|
私有性 | 弱 | 强 | 较强 | 极强 |
易用性 | 简单 | 一般 | 一般 | 简单 |
性能 | 高 | 较低 | 较高 | 高 |
代码复杂度 | 低 | 较高 | 一般 | 低 |
是否原生支持 | 否 | 否 | 否 | 是 |
子类访问父类私有 | 可以 | 不可以 | 不可以 | 不可以 |
适用场景 | 小型项目,不强调安全性 | 需要强私有性 | 需要一定性能 | 大型项目,强调安全性,需要原生支持 |
第六幕:一些注意事项
- 浏览器兼容性: ES2022 的特性需要较新的浏览器版本支持。在使用前请确保目标环境支持。
- TypeScript 的配合: TypeScript 也支持私有属性和方法,并且提供了更丰富的类型检查功能。
- 谨慎使用私有: 过度使用私有可能会降低代码的灵活性。应该根据实际情况,选择合适的封装级别。
第七幕:案例分析:一个简单的银行账户
让我们用一个银行账户的例子来演示如何使用 Private Methods
和 Private Accessors
。
class BankAccount {
#balance = 0; // 私有余额
constructor(initialBalance) {
if (initialBalance > 0) {
this.#balance = initialBalance;
} else {
console.warn("Initial balance must be positive.");
}
}
#validateAmount(amount) { // 私有方法,验证金额
if (typeof amount !== 'number' || amount <= 0) {
console.error("Invalid amount.");
return false;
}
return true;
}
deposit(amount) {
if (this.#validateAmount(amount)) {
this.#balance += amount;
console.log(`Deposited ${amount}. New balance: ${this.getBalance()}`);
}
}
withdraw(amount) {
if (this.#validateAmount(amount)) {
if (amount <= this.#balance) {
this.#balance -= amount;
console.log(`Withdrew ${amount}. New balance: ${this.getBalance()}`);
} else {
console.error("Insufficient funds.");
}
}
}
getBalance() {
return this.#balance;
}
// 私有 accessor ,隐藏内部计息逻辑
get #interestRate() {
return 0.01; // 假设利率为 1%
}
calculateInterest() {
return this.#balance * this.#interestRate;
}
// 试图直接访问私有属性
// externalAccess() {
// console.log(this.#interestRate); //报错
// }
}
const account = new BankAccount(100);
account.deposit(50);
account.withdraw(20);
console.log(`Current balance: ${account.getBalance()}`);
console.log(`Interest earned: ${account.calculateInterest()}`);
//account.#balance = 1000; // 报错:无法访问私有属性
在这个例子中,#balance
是私有字段,#validateAmount
是私有方法,#interestRate
是私有accessor。它们只能在 BankAccount
类内部访问,保证了账户余额的安全性,以及验证金额的逻辑不被外部篡改。
总结:拥抱“真·私有”,编写更健壮的代码
Private Methods
和 Private Accessors
是 JavaScript 封装能力的重要补充。它们提供了一种更可靠、更原生的方式来隐藏类的内部实现细节,编写出更加健壮、易于维护的代码。赶紧用起来,让你的代码更上一层楼吧!
希望今天的分享对大家有所帮助!下次再见!