各位观众老爷们,晚上好!今儿咱们聊点儿高级的,关于 JavaScript 中用 WeakMap
实现私有属性的那些事儿。别害怕,虽然听着高大上,但其实道理很简单,咱们争取用最接地气的方式把它讲明白。
开场白:为啥需要私有属性?
在面向对象编程的世界里,封装是个很重要的概念。简单来说,就是把数据和操作数据的代码打包在一起,形成一个对象。为了保证对象的内部数据安全,防止外部随意修改,我们需要控制哪些属性可以被外部访问,哪些属性只能在对象内部使用。这就是私有属性的意义所在。
想象一下,你设计了一个银行账户类,账户余额肯定不能随便让外部修改吧?不然谁都能给自己账户里添几个亿,那银行还不得破产啊!所以,余额就应该是一个私有属性,只能通过特定的方法(比如存款、取款)来修改。
JavaScript 的私有属性演变史:一场充满妥协的旅程
JavaScript 在早期并没有提供真正的私有属性机制。开发者们为了实现类似的效果,可谓是绞尽脑汁,想出了各种奇葩的方案。
-
约定俗成法:下划线命名
最简单粗暴的方法就是在私有属性名前面加上一个下划线
_
。比如:class BankAccount { constructor(initialBalance) { this._balance = initialBalance; // 下划线表示私有属性 } deposit(amount) { this._balance += amount; } getBalance() { return this._balance; } } const account = new BankAccount(1000); console.log(account._balance); // 仍然可以访问,只是不建议 account._balance = -1000000; // 仍然可以修改,非常危险 console.log(account.getBalance());
这种方法完全依赖开发者的自觉性,并不能真正阻止外部访问和修改私有属性。说白了,就是君子协定,小人可以随便破坏。
-
闭包大法:利用作用域
利用闭包的特性,可以将私有属性定义在构造函数的作用域内,外部无法直接访问。
function BankAccount(initialBalance) { let balance = initialBalance; // 私有属性 this.deposit = function(amount) { balance += amount; }; this.getBalance = function() { return balance; }; } const account = new BankAccount(1000); // console.log(account.balance); // 报错,无法访问 account.deposit(500); console.log(account.getBalance());
这种方法可以实现真正的私有属性,但也有缺点:
- 每个实例都会创建一份
deposit
和getBalance
方法,造成内存浪费。 - 无法使用
prototype
添加共享方法,因为私有属性只能在构造函数内部访问。
- 每个实例都会创建一份
-
ES6 的 Symbol:半真半假的私有属性
ES6 引入了
Symbol
,可以用它来定义独一无二的属性名,从而避免属性名冲突。可以利用这个特性来模拟私有属性。const _balance = Symbol('balance'); class BankAccount { constructor(initialBalance) { this[_balance] = initialBalance; } deposit(amount) { this[_balance] += amount; } getBalance() { return this[_balance]; } } const account = new BankAccount(1000); // console.log(account[_balance]); // 报错,无法直接访问 account.deposit(500); console.log(account.getBalance()); // 但是可以通过 Reflect.ownKeys() 找到 Symbol 属性 console.log(Reflect.ownKeys(account)); // [Symbol(balance)] // 仍然可以通过 Symbol 访问和修改 const balanceSymbol = Reflect.ownKeys(account)[0]; account[balanceSymbol] = -999999; console.log(account.getBalance());
Symbol
确实可以避免属性名冲突,但仍然可以通过Reflect.ownKeys()
等方法找到Symbol
属性,并进行访问和修改。所以,它并不是真正的私有属性,只能算是一种约定。
WeakMap
:真正的私有属性实现
WeakMap
是一种特殊的 Map
,它的键必须是对象,而且是弱引用。这意味着,如果一个对象只被 WeakMap
引用,那么当垃圾回收器运行时,这个对象就会被回收,WeakMap
中对应的键值对也会被移除。
利用 WeakMap
的这个特性,我们可以将私有属性存储在 WeakMap
中,以对象实例作为键,私有属性作为值。这样,只有拥有 WeakMap
的代码才能访问和修改私有属性,外部无法直接访问。
const _balance = new WeakMap();
class BankAccount {
constructor(initialBalance) {
_balance.set(this, initialBalance); // 将实例作为键,余额作为值
}
deposit(amount) {
const currentBalance = _balance.get(this);
_balance.set(this, currentBalance + amount);
}
getBalance() {
return _balance.get(this);
}
}
const account = new BankAccount(1000);
// console.log(account._balance); // 无法访问
// console.log(_balance.get(account)); // 无法从外部访问 WeakMap
account.deposit(500);
console.log(account.getBalance());
WeakMap
实现私有属性的优势:
- 真正的私有性: 外部无法直接访问和修改私有属性。
- 避免内存泄漏: 当对象实例被回收时,
WeakMap
中对应的键值对也会被移除,避免内存泄漏。 - 代码清晰: 将私有属性的存储和访问逻辑集中在
WeakMap
中,代码更加清晰易懂。 -
可以配合
prototype
使用: 可以把方法定义在prototype
上,同时访问WeakMap
中的私有属性。const _balance = new WeakMap(); class BankAccount { constructor(initialBalance) { _balance.set(this, initialBalance); } deposit(amount) { this.depositInternal(amount); } getBalance() { return _balance.get(this); } depositInternal(amount) { const currentBalance = _balance.get(this); _balance.set(this, currentBalance + amount); } } BankAccount.prototype.depositInternal = function(amount) { const currentBalance = _balance.get(this); _balance.set(this, currentBalance + amount); } const account = new BankAccount(1000); account.deposit(500); console.log(account.getBalance());
更进一步:使用 IIFE 封装 WeakMap
为了进一步提高代码的安全性,可以将 WeakMap
封装在一个立即执行函数表达式 (IIFE) 中,防止外部访问。
const BankAccount = (function() {
const _balance = new WeakMap();
class BankAccount {
constructor(initialBalance) {
_balance.set(this, initialBalance);
}
deposit(amount) {
const currentBalance = _balance.get(this);
_balance.set(this, currentBalance + amount);
}
getBalance() {
return _balance.get(this);
}
}
return BankAccount;
})();
const account = new BankAccount(1000);
account.deposit(500);
console.log(account.getBalance());
// console.log(_balance.get(account)); // 报错,无法访问 WeakMap
使用 WeakMap
实现私有属性的注意事项:
WeakMap
的键必须是对象。WeakMap
是弱引用,当对象实例被回收时,WeakMap
中对应的键值对也会被移除。- 需要维护一个
WeakMap
来存储私有属性。 - 不能通过
Object.keys()
、Object.values()
、Object.entries()
等方法访问WeakMap
中的键值对。
各种私有属性实现方案对比:
方法 | 私有性 | 优点 | 缺点 |
---|---|---|---|
下划线命名 | 弱 | 简单易懂 | 完全依赖开发者自觉性,无法真正阻止外部访问和修改 |
闭包 | 强 | 真正的私有属性 | 每个实例都会创建一份方法,造成内存浪费;无法使用 prototype 添加共享方法 |
Symbol | 弱 | 可以避免属性名冲突 | 仍然可以通过 Reflect.ownKeys() 等方法找到 Symbol 属性,并进行访问和修改 |
WeakMap |
强 | 真正的私有属性;避免内存泄漏;代码清晰;可以配合 prototype 使用 |
需要维护一个 WeakMap ;不能通过 Object.keys() 等方法访问 WeakMap 中的键值对 |
Typescript private | 编译时检查 | 简单易用 | 编译成JS后,还是会转换成 _ 命名的方式,只是在编译时会报错,并不能真正阻止外部访问和修改。 |
# (ES2022) |
强 | 内置原生支持,语法简洁。 | 目前支持度不如 WeakMap , 且需要babel转换。 |
ES2022 的 #
私有属性:原生支持,更加简洁
ES2022 引入了 #
私有字段,提供了原生的私有属性支持。使用 #
定义的属性只能在类内部访问。
class BankAccount {
#balance; // 私有属性
constructor(initialBalance) {
this.#balance = initialBalance;
}
deposit(amount) {
this.#balance += amount;
}
getBalance() {
return this.#balance;
}
}
const account = new BankAccount(1000);
// console.log(account.#balance); // 报错,无法访问
account.deposit(500);
console.log(account.getBalance());
#
私有属性的优势:
- 原生支持: 不需要额外的库或技巧,语法更加简洁。
- 真正的私有性: 外部无法直接访问和修改私有属性。
#
私有属性的局限性:
- 目前可能需要 Babel 转换。
- 不能在类外部访问私有属性。
总结:选择适合你的私有属性方案
在 JavaScript 中实现私有属性有很多种方法,每种方法都有其优缺点。选择哪种方法取决于你的具体需求和偏好。
- 如果只是想遵循一些约定,避免属性名冲突,可以使用下划线命名或
Symbol
。 - 如果需要真正的私有属性,可以使用
WeakMap
或 ES2022 的#
私有字段。 - 如果需要兼容旧版本的浏览器,建议使用
WeakMap
。 - 如果使用 TypeScript,可以使用
private
关键字,但在编译成 JavaScript 后,仍然会使用下划线命名。
最后,希望今天的讲座对大家有所帮助!记住,没有银弹,选择最适合你的方案才是王道!