考虑一个函数的传统方式是,你声明一个函数,并在它内部添加代码。但是相反的想法也同样强大和有用:拿你所编写的代码的任意一部分,在它周围包装一个函数声明,这实质上“隐藏”了这段代码。

其实际结果是在这段代码周围创建了一个作用域气泡,这意味着现在在这段代码中的任何声明都将绑在这个新的包装函数的作用域上,而不是前一个包含它们的作用域。换句话说,你可以通过将变量和函数围在一个函数的作用域中来“隐藏”它们。

为什么“隐藏”变量和函数是一种有用的技术?

有多种原因驱使着这种基于作用域的隐藏。它们主要是由一种称为“最低权限原则”的软件设计原则引起的[^note-leastprivilege],有时也被称为“最低授权”或“最少曝光”。这个原则规定,在软件设计中,比如一个模块/对象的API,你应当只暴露所需要的最低限度的东西,而“隐藏”其他的一切。

这个原则可以扩展到用哪个作用域来包含变量和函数的选择。如果所有的变量和函数都在全局作用域中,它们将理所当然地对任何嵌套的作用域来说都是可访问的。但这回违背“最少……”原则,因为你(很可能)暴露了许多你本应当保持为私有的变量和函数,而这些代码的恰当用法是不鼓励访问这些变量/函数的。

例如:

  1. function doSomething(a) {
  2. b = a + doSomethingElse( a * 2 );
  3. console.log( b * 3 );
  4. }
  5. function doSomethingElse(a) {
  6. return a - 1;
  7. }
  8. var b;
  9. doSomething( 2 ); // 15

在这个代码段中,变量 b 和函数 doSomethingElse(..) 很可能是 doSomething(..) 如何工作的“私有”细节。允许外围的作用域“访问” bdoSomethingElse(..) 不仅没必要而且可能是“危险的”,因为它们可能会以种种意外的方式,有意或无意地被使用,而这也许违背了 doSomething(..) 假设的前提条件。

一个更“恰当”的设计是讲这些私有细节隐藏在doSomething(..)的作用域内部,比如:

  1. function doSomething(a) {
  2. function doSomethingElse(a) {
  3. return a - 1;
  4. }
  5. var b;
  6. b = a + doSomethingElse( a * 2 );
  7. console.log( b * 3 );
  8. }
  9. doSomething( 2 ); // 15

现在,bdoSomethingElse(..) 对任何外界影响都是不可访问的,而是仅仅由 doSomething(..) 控制。它的功能和最终结果不受影响,但是这种设计将私有细节保持为私有的,这通常被认为是好的软件。

避免冲突

将变量和函数“隐藏”在一个作用域内部的另一个好处是,避免两个同名但用处不同的标识符之间发生无意的冲突。冲突经常导致值被意外地覆盖。

例如:

  1. function foo() {
  2. function bar(a) {
  3. i = 3; // 在外围的for循环的作用域中改变`i`
  4. console.log( a + i );
  5. }
  6. for (var i=0; i<10; i++) {
  7. bar( i * 2 ); // 噢,无限循环!
  8. }
  9. }
  10. foo();

bar(..) 内部的赋值 i = 3 意外地覆盖了在 foo(..) 的for循环中声明的 i。在这个例子中,这将导致一个无限循环,因为 i 被设定为固定的值 3,而它将永远 < 10

bar(..) 内部的赋值需要声明一个本地变量来使用,不论选用什么样的标识符名称。var i = 3; 将修复这个问题(并将为 i 创建一个前面提到的“遮蔽变量”声明)。一个 另外的 选项,不是代替的选项,是完全选择另外一个标识符名称,比如 var j = 3;。但是你的软件设计也许会自然而然地使用相同的标识符名称,所以在这种情况下利用作用域来“隐藏”你的内部声明是你最好/唯一的选择。

全局“名称空间”

变量冲突(很可能)发生的一个特别强有力的例子是在全局作用域中。当多个库被加载到你的程序中时,如果它们没有适当地隐藏它们的内部/私有函数和变量,那么它们可以十分容易地互相冲突。

这样的库通常会在全局作用域中使用一个足够独特的名称来创建一个单独的变量声明,它经常是一个对象。然后这个对象被用作这个库的一个“名称空间”,所有要明确暴露出来的功能都被作为属性挂在这个对象(名称空间)上,而不是将它们自身作为顶层词法作用域的标识符。

例如:

  1. var MyReallyCoolLibrary = {
  2. awesome: "stuff",
  3. doSomething: function() {
  4. // ...
  5. },
  6. doAnotherThing: function() {
  7. // ...
  8. }
  9. };

模块管理

另一种回避冲突的选择是通过任意一种依赖管理器,使用更加现代的“模块”方式。使用这些工具,没有库可以向全局作用域添加任何标识符,取而代之的是使用依赖管理器的各种机制,要求库的标识符被明确地导入到另一个指定的作用域中。

应该可以看到,这些工具并不拥有可以豁免于词法作用域规则的“魔法”功能。它们简单地使用这里讲解的作用域规则,来强制标识符不会被注入任何共享的作用域,而是保持在私有的,不易冲突的作用域中,这防止了任何意外的作用域冲突。

因此,如果你选择这样做的话,你可以防御性地编码,并在实际上不使用依赖管理器的情况下,取得与使用它们相同的结果。关于模块模式的更多信息参见第五章。