在 JavaScript 中实现私有变量

Avatar of Khari McMillian
Khari McMillian

DigitalOcean 为您的旅程的每个阶段提供云产品。立即开始使用 $200 免费信用额度!

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 正式发布,并带来了两个新的变量关键字:letconst。两者都是块作用域的,这意味着使用这些关键字创建的变量可以被同一对花括号内的任何代码访问。与 var 相同,但 letconst 变量不能被循环、函数、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());

首先要突出的一点是,milesDrivenspeed 变量在 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 代码中应用有所了解。这是一种强大的技术,可以支持多种不同的方法,并使你的代码更加可用和无错误。自己尝试一些新的示例,获得更好的体验。