JavaScript(或 ECMAScript)是为网页提供动力的编程语言。它由 Brendan Eich 于 1995 年 5 月创建,并已成为一种广泛使用且用途广泛的技术。尽管它取得了成功,但它也遇到了不少批评,尤其是针对其特殊性。例如,对象在用作索引时被强制转换为字符串形式,1 == "1"
返回 true,或者臭名昭著的令人困惑的 this
关键字。但是,一个特别有趣的怪癖是,存在各种用于变量隐私的技术。
在当前状态下,JavaScript 中没有“直接”的方法来创建私有变量。在其他语言中,您可以使用 private
关键字或双下划线,一切都会正常工作,但在 JavaScript 中,变量隐私具有使其看起来更像是语言的突现特性而不是预期功能的特性。让我们介绍一下我们问题的背景。
“var” 关键字
在 2015 年之前,基本上只有一种方法可以创建变量,那就是 var
关键字。var
是函数作用域的,这意味着使用该关键字实例化的变量只能被函数内的代码访问。当在函数之外或“全局”时,该变量将可被定义该变量后的任何代码访问。如果您尝试在定义之前在同一作用域中访问该变量,您将得到 undefined
而不是错误。这是由于 var
关键字的“提升”方式。
// Define "a" in global scope
var a = 123;
// Define "b" in function scope
(function() {
console.log(b); //=> Returns "undefined" instead of an error due to hoisting.
var b = 456;
})();
console.log(a); // => 123
console.log(b); // Throws "ReferenceError" exception, because "b" cannot be accessed from outside the function scope.
ES6 变量的诞生
在 2015 年,ES6/ES2015 正式发布,并带来了两个新的变量关键字:let
和 const
。两者都是块作用域的,这意味着使用这些关键字创建的变量可以被同一对花括号内的任何代码访问。与 var
相同,但 let
和 const
变量不能被循环、函数、if 语句、花括号等块作用域外的代码访问。
const a = 123;
// Block scope example #1
if (true) {
const b = 345;
}
// Block scope example #2
{
const c = 678;
}
console.log(a); // 123
console.log(b); // Throws "ReferenceError" because "b" cannot be accessed from outside the block scope.
console.log(c); // Throws "ReferenceError" because "b" cannot be accessed from outside the block scope.
由于作用域外的代码无法访问这些变量,我们获得了隐私的突现特性。我们将介绍一些在不同方式实现它的技术。
使用函数
由于 JavaScript 中的函数也是块,所有变量关键字都可以与它们一起使用。此外,我们可以实现一个非常有用的设计模式,称为“模块”。
模块设计模式
谷歌依靠 牛津词典 来定义“模块”。
程序可以从中构建或可以分析复杂活动的任何一个独立但相互关联的单元。
—”模块”定义 1.2
模块设计模式在 JavaScript 中非常有用,因为它结合了公共和私有组件,并且它允许我们将程序分解成更小的组件,只公开另一个程序部分应该能够通过称为“封装”的过程访问的内容。通过这种方法,我们只公开需要使用的东西,并可以隐藏不需要看到的其余实现。我们可以利用函数作用域来实现这一点。
const CarModule = () => {
let milesDriven = 0;
let speed = 0;
const accelerate = (amount) => {
speed += amount;
milesDriven += speed;
}
const getMilesDriven = () => milesDriven;
// Using the "return" keyword, you can control what gets
// exposed and what gets hidden. In this case, we expose
// only the accelerate() and getMilesDriven() function.
return {
accelerate,
getMilesDriven
}
};
const testCarModule = CarModule();
testCarModule.accelerate(5);
testCarModule.accelerate(4);
console.log(testCarModule.getMilesDriven());
有了它,我们可以获得行驶的英里数,以及加速度,但由于用户在这种情况下不需要访问速度,我们可以通过只公开 accelerate()
和 getMilesDriven()
方法来隐藏它。本质上,speed
是一个私有变量,因为它只能被同一块作用域内的代码访问。在这种情况下,私有变量的好处开始变得清晰。当您删除访问变量、函数或任何其他内部组件的能力时,您会减少因其他人错误使用不应使用的东西而导致的错误的表面积。
另一种方法
在这个第二个示例中,您会注意到添加了 this
关键字。ES6 箭头函数(=>
)和传统的 function(){}
之间存在差异。使用 function
关键字,您可以使用 this
,它将绑定到 function
本身,而箭头函数不允许任何类型的 this
关键字的使用。两者都是创建模块的有效方法。核心思想是公开应该访问的部分,并保留不应交互的部分,因此既有公共数据也有私有数据。
function CarModule() {
let milesDriven = 0;
let speed = 0;
// In this case, we instead use the "this" keyword,
// which refers to CarModule
this.accelerate = (amount) => {
speed += amount;
milesDriven += speed;
}
this.getMilesDriven = () => milesDriven;
}
const testCarModule = new CarModule();
testCarModule.accelerate(5);
testCarModule.accelerate(4);
console.log(testCarModule.getMilesDriven());
进入 ES6 类
类是 ES6 附加的另一个功能。类本质上是语法糖——换句话说,仍然是函数,但可能被“糖化”成更易于表达的形式。使用类,变量隐私(截至目前)在不进行一些重大代码更改的情况下几乎是不可能的。
让我们看一个类示例。
class CarModule {
/*
milesDriven = 0;
speed = 0;
*/
constructor() {
this.milesDriven = 0;
this.speed = 0;
}
accelerate(amount) {
this.speed += amount;
this.milesDriven += this.speed;
}
getMilesDriven() {
return this.milesDriven;
}
}
const testCarModule = new CarModule();
testCarModule.accelerate(5);
testCarModule.accelerate(4);
console.log(testCarModule.getMilesDriven());
首先要突出的一点是,milesDriven
和 speed
变量在 constructor()
函数内。请注意,您也可以在构造函数之外定义变量(如代码注释中所示),但无论如何,它们在功能上都是相同的。问题是这些变量将是公开的,可以被类外部的元素访问。
让我们看看一些解决这个问题的方法。
使用下划线
在需要隐私来防止合作者犯下一些灾难性错误的情况下,在变量前加上下划线(_
),尽管仍然对外部“可见”,但足以向开发人员发出“不要碰这个变量”的信号。因此,例如,我们现在有了以下内容
// This is the new constructor for the class. Note that it could
// also be expressed as the following outside of constructor().
/*
_milesDriven = 0;
_speed = 0;
*/
constructor() {
this._milesDriven = 0;
this._speed = 0;
}
虽然这对于它的特定用例确实
有效,但仍然可以说它在许多层面上都远非理想。您仍然可以访问该变量,但您还需要修改变量名称。
将所有内容放在构造函数内
从技术上讲,确实存在一种在类中用于变量隐私的方法,您可以立即使用,那就是将所有变量和方法放在 constructor()
函数内。让我们看一下。
class CarModule {
constructor() {
let milesDriven = 0;
let speed = 0;
this.accelerate = (amount) => {
speed += amount;
milesDriven += speed;
}
this.getMilesDriven = () => milesDriven;
}
}
const testCarModule = new CarModule();
testCarModule.accelerate(5);
testCarModule.accelerate(4);
console.log(testCarModule.getMilesDriven());
console.log(testCarModule.speed); // undefined -- We have true variable privacy now.
这种方法实现了真正的变量隐私,因为没有办法直接访问没有故意公开的任何变量。问题是我们现在有了,嗯,与我们之前的代码相比,看起来不太好的代码,此外,它还损害了我们使用类获得的语法糖的好处。在这一点上,我们不妨使用 function()
方法。
使用 WeakMap
实现私有变量还有另一种更具创意的方法,那就是使用 WeakMap()
。虽然听起来和 Map
很像,但两者有很大区别。Map
可以接受任何类型的值作为键,而 WeakMap
只能接受对象,并在对象键被垃圾回收时删除 WeakMap
中的值。此外,WeakMap
不能被迭代,这意味着你必须访问对象键的引用才能访问值。这使得它非常适合创建私有变量,因为这些变量实际上是不可见的。
class CarModule {
constructor() {
this.data = new WeakMap();
this.data.set(this, {
milesDriven: 0,
speed: 0
});
this.getMilesDriven = () => this.data.get(this).milesDriven;
}
accelerate(amount) {
// In this version, we instead create a WeakMap and
// use the "this" keyword as a key, which is not likely
// to be used accidentally as a key to the WeakMap.
const data = this.data.get(this);
const speed = data.speed + amount;
const milesDriven = data.milesDriven + data.speed;
this.data.set({ speed, milesDriven });
}
}
const testCarModule = new CarModule();
testCarModule.accelerate(5);
testCarModule.accelerate(4);
console.log(testCarModule.getMilesDriven());
console.log(testCarModule.data); //=> WeakMap { [items unknown] } -- This data cannot be accessed easily from the outside!
这种方法可以有效地防止意外使用数据,但它并非真正的私有,因为可以通过将 this
替换为 CarModule
来从外部访问它。此外,它在代码中增加了相当大的复杂性,因此并不是最优雅的解决方案。
使用符号来防止冲突
如果目的是为了防止名称冲突,可以使用 Symbol
提供一个有效的解决方案。它们本质上是实例,可以作为唯一值,永远不会与其他任何东西相等,除了它自己的唯一实例。以下是一个示例:
class CarModule {
constructor() {
this.speedKey = Symbol("speedKey");
this.milesDrivenKey = Symbol("milesDrivenKey");
this[this.speedKey] = 0;
this[this.milesDrivenKey] = 0;
}
accelerate(amount) {
// It's virtually impossible for this data to be
// accidentally accessed. By no means is it private,
// but it's well out of the way of anyone who would
// be implementing this module.
this[this.speedKey] += amount;
this[this.milesDrivenKey] += this[this.speedKey];
}
getMilesDriven() {
return this[this.milesDrivenKey];
}
}
const testCarModule = new CarModule();
testCarModule.accelerate(5);
testCarModule.accelerate(4);
console.log(testCarModule.getMilesDriven());
console.log(testCarModule.speed); // => undefined -- we would need to access the internal keys to access the variable.
Like the underscore solution, this method more or less relies on naming conventions to prevent confusion.
TC39 私有类字段提案
最近,提出了一个新提案,该提案将为类引入私有变量。这很简单:在变量名前面加上一个 #
,它就变成了私有的。无需额外的结构更改。
class CarModule {
#speed = 0
#milesDriven = 0
accelerate(amount) {
// It's virtually impossible for this data to be
// accidentally accessed.
this.#speed += amount;
this.#milesDriven += speed;
}
getMilesDriven() {
return this.#milesDriven;
}
}
const testCarModule = new CarModule();
testCarModule.accelerate(5);
testCarModule.accelerate(4);
console.log(testCarModule.getMilesDriven());
console.log(testCarModule.speed); //=> undefined -- we would need to access the internal keys to access the variable.
私有类字段提案尚未成为标准,在撰写本文时,如果没有使用 Babel 则无法实现,因此你将不得不等待一段时间才能在主流浏览器、Node 等环境中使用它。
结论
以上总结了在 JavaScript 中实现私有变量的各种方法。没有一种方法是“正确”的。这些方法适用于不同的需求、现有代码库和其他约束。虽然每种方法都有其优缺点,但最终,只要它们能有效地解决你的问题,所有方法都同样有效。
感谢阅读!我希望这能让你对作用域和变量隐私如何在 JavaScript 代码中应用有所了解。这是一种强大的技术,可以支持多种不同的方法,并使你的代码更加可用和无错误。自己尝试一些新的示例,获得更好的体验。
你的 WeakMap 和符号示例实际上仍然暴露了一切…… 当然,它们被一层间接性隐藏了,但仍然如此。
试试类似这样的方法
这实际上强制执行了数据的隐私,因为弱映射只对 CarModule 的实例可见。
使用符号的方法也是如此——将它们定义在一个非全局的范围内,而不是作为它们所用对象的属性。(没有理由每次都重新定义符号:Symbol(“Foo”) !== Symbol(“Foo”).)
我所见过的在 ES6 类中实现私有变量的方法是使用符号,但不要将它们暴露在实例上。
var Person = Class.create(“Person”, {
ctor: function(fn, ln){
this.fn = fn;
this.ln = ln;
},
firstName: function (){
if(arguments.length){
this.fn = arguments [0];}
return this.fn;
}
});
var p = new Person(‘John’, ‘Doe’);
p.firstName();
p.fn 在对象实例上不可用
很棒的 JavaScript 数据代码信息实现和执行。不同类型的条件和情况的各种级别。
关于提出的语法,它已经处于第 3 阶段近两年了,并且已经在 Chrome 和 Node.js 中得到了支持。在我看来,这是一个板上钉钉的事。那些需要此功能的人今天可以通过 Babel 使用它,没有任何问题。
使用 es 代理来代理你创建的对象的任何类型的读写操作。
为什么不直接使用一个私有的关键字?# 在很多其他语言中被用作注释。
感谢你的文章!我学到了一些东西,特别是 WeakMap 和 TC39 提案。在你的 WeakMap 示例的
accellerate
方法中,我认为你有一个错别字。难道不应该将this
作为第一个参数传递给this.data.set
吗?为什么要引入全新的语法?
Typescript、C++、C#、PHP、Java 等…… 都是使用“private/protected”关键字来实现此功能的。
请不要再建议另一种方法。当我们已经拥有如此多的语言需要使用时,最好在语法方面保持一致性,而不是减少一致性。
Typescript 提供了
private
关键字,它会在你尝试使用私有属性时在编译时发出警报。+1
我使用私有变量的模式是这样的
约定是将私有类属性以下划线开头,这样可以轻松区分类私有变量和函数局部变量,并且修改公共属性是通过
_this.state
而不是this.state
完成的——从而统一了私有和公共函数的使用。