模块与类 - 如何在 JavaScript 中管理隐私
面向对象范式彻底改变了开发者的思维方式和代码编写方式,即便你不喜欢它或它的前提。这种将数据和过程封装成属性和方法的范式虽然并非全新,但它影响了许多新兴语言,即便其中一些语言并未将其作为主要范式。
像 C++、Java、Python 甚至 JavaScript 这样的语言都被认为是实现了面向对象编程 (OOP) 范式的语言。正如我们将要讨论的,JavaScript 有其自身处理对象的方式,并具有一些特殊性。但首先,我们需要讨论一个起点:JavaScript 中有一个关键事实与 OOP 的方向背道而驰:那就是缺乏封装。
这里还有一个包含一些测试的仓库!快去看看吧!
类和对象
类是对数据类型的定义:它们存储/隐藏哪些数据以及它们的行为方式。一个类的实例可以执行函数(作为方法)并将数据存储为属性。这些实例就是所谓的对象,它们存在于程序的运行时环境中。
面向对象编程 (OOP) 的一个重要特性是对象应该能够封装(隐藏)其数据。这意味着,如果有人试图访问对象的某些信息,而类又明确声明了这一点,那么就不应该能够访问到这些信息。请看以下示例:
假设埃隆·马斯克🧑💼创造了一个很棒的产品Trash Can,它可以执行3个简单的任务:
- 把一件“垃圾”扔进垃圾桶
- 一次性清理垃圾桶内的所有物品。
- 屏幕上有一个按钮,用于显示垃圾桶是否已完全清空。
它的界面TrashCan大致如下:
TrashCan {
throwAway(item);
clean();
isEmpty();
}
由于 JavaScript 有该class关键字,因此可以考虑Trash以下一种实现方式。
class TrashCan {
constructor() {
this.items = [];
}
throwAway(item) {
this.items = [...this.items, item];
}
clean() {
this.items = [];
}
isEmpty() {
return this.items.length === 0;
}
}
var elonTrashCan = new TrashCan();
现在它elonTrashCan已清空,准备开始工作。但是执行过程中会发生什么呢?
elonTrashCan.throwAway('paper ball');
elonTrashCan.throwAway('empty Starbucks cup of coffee');
elonTrashCan.throwAway('empty package of Cookies');
elonTrashCan.clean();
elonTrashCan.items = ['SpaceX secret project'];
console.log(elonTrashCan.isEmpty()); // --> ???
- 埃隆·马斯克🧑💼肯定会因为我们弄坏了他的垃圾桶而生我们的气。
elonTrashCan.isEmpty()将会返回false,因为我们定义了elonTrashCan.items一个包含 1 个元素的项。elonTrashCan.items由于无法访问,因此elonTrashCan.isEmpty()调用将返回true
答案是选项 2。即使没有在外部显式声明,也可以访问items对象实例内部。itemsconstructor
以上述示例为例,并考虑一种理想的面向对象语言实现,执行该操作elonTrashCan.items应该会导致程序因试图访问私有属性而报错。但在 JavaScript 中,这些调用是合法有效的,并且不会导致任何错误。
那么,在 JavaScript 中是否无法实现隐私保护?有没有办法隐藏对象外部的数据,只暴露public数据本身?
模块模式
好消息是,JavaScript 中有一种行为可以提供与隐私相关的功能:闭包。如果您感兴趣,可以阅读这篇关于闭包的文章。
使用闭包来隐藏变量和函数是一种很好的方法,可以将数据封装在一个实例中,并只公开所需的接口。
但这究竟是如何运作的呢?
让我们创建一个与 Elon Musk 🧑💼TrashCan对象相同的函数,并仅返回其公共接口,如下所示。
const TrashCan = () => {
let items = [];
const throwAway = item => {
items = [...items, item];
}
const clean = () => {
items = [];
}
const isEmpty = () => {
return items.length === 0;
}
return {
throwAway,
clean,
isEmpty,
}
}
var elonTrashCan = TrashCan();
接下来,elonTrashCan我们尝试执行与上面相同的代码。
elonTrashCan.throwAway('paper ball');
elonTrashCan.throwAway('empty Starbucks cup of coffee');
elonTrashCan.throwAway('empty package of Cookies');
elonTrashCan.clean();
elonTrashCan.items = ['SpaceX secret project'];
console.log(elonTrashCan.isEmpty()); // --> ???
- 埃隆·马斯克🧑💼肯定会因为我们弄坏了他的第二个垃圾桶而更加生气。
elonTrashCan.isEmpty()将会返回false,因为我们再次定义了elonTrashCan.items一个包含 1 个元素的项。elonTrashCan.items由于无法访问,因此elonTrashCan.isEmpty()调用将返回true
实际上,发生了一件非常奇怪的事情:
elonTrashCan.isEmpty()返回false原因是我们的内部items为空elonTrashCan.items里面有 1 件物品
使用这种方法,可以“限制”外部对特定接口的访问,使其只能访问所需的接口,而接口内部则隐藏着内容。另一方面,JavaScript 允许在运行时定义新的属性,即使属性名称与闭包中使用的名称相同。
闭包内部的代码不会依赖于这些新属性,因为原始属性存储在闭包内部,无法访问。至此,最初的目标——隐私——已经实现。模块模式不仅适用于属性,也可用于隐藏方法。
对于创建新属性可能带来的副作用,强烈建议不要更改原始接口,甚至在使用这些属性之前进行一些测试,例如:
if(typeof elonTrashCan.items === 'undefined') {
console.log('No exposed items!') // --> No exposed items!
}
包起来
在对面向对象范式和 JavaScript 的类实现进行了一些讨论之后,如果你不使用像 Babel 这样的转译器,那么 JS 类可能不是创建带有私有数据的对象的最佳选择。
使用闭包和模块模式,可以在 JavaScript 中以简单且可重用的方式实现隐私保护class。如果必须实现,请考虑使用转译器或采用更健壮的方法并结合模块模式。我们强烈不建议使用转译器!
即使存在一些明显的损失,例如,即使使用模块inheritance,仍然有有效的方法来实现这些好处。
我漏掉了什么吗?你觉得有什么不清楚的地方吗?欢迎在评论区留言或私信我,我们一起讨论!


