你不懂js系列学习笔记-作用域和闭包- 03

第3章:函数与块儿作用域

原文:You-Dont-Know-JS

正如我们在第二章中探索的,作用域由一系列“气泡”组成,这些“气泡”的每一个就像一个容器或篮子,标识符(变量,函数)就在它里面被声明。这些气泡整齐地互相嵌套在一起,而且这种嵌套是在编写时定义的。

创造一个新的气泡方式:

  1. 函数
  2. with: 从对象中创建的作用域仅存在于这个 with 语句的生命周期中,而不在外围作用域中。
  3. try/catch: JavaScript 在 ES3 中明确指出在 try/catchcatch 子句中声明的变量,是属于 catch 块儿的块儿作用域的。

1 函数中的作用域

你可以通过将变量和函数围在一个函数的作用域中来“隐藏”它们:

function foo(a) {
  var b = 2;
  // 一些代码
  function bar() {
    // ...
  }
  // 更多代码
  var c = 3;
}
// foo() 外面访问不到 a,b,c
复制代码

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

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

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

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

例如:

function doSomething(a) {
  b = a + doSomethingElse(a * 2);
  console.log(b * 3);
}

function doSomethingElse(a) {
  return a - 1;
}

var b;

doSomething(2); // 15
复制代码

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

修改:

function doSomething(a) {
  function doSomethingElse(a) {
    return a - 1;
  }

  var b;

  b = a + doSomethingElse(a * 2);

  console.log(b * 3);
}

doSomething(2); // 15
复制代码

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

例如:

function foo() {
  function bar(a) {
    i = 3; // 在外围的for循环的作用域中改变`i`
    console.log(a + i);
  }

  for (var i = 0; i < 10; i++) {
    bar(i * 2); // 噢,无限循环!
  }
}

foo();
复制代码

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

通过将变量和函数围在一个函数的作用域中“可以工作”,但它不一定非常理想。它引入了几个问题。首先是我们不得不声明一个命名函数 foo(),这意味着这个标识符名称 foo 本身就“污染”了外围作用域(在这个例子中是全局)。我们要不得不通过名称(foo())明确地调用这个函数来使被包装的代码真正运行。

解决方法(立即调用函数表达式 IIFE ):

var a = 2;

(function IIFE(global) {

  var a = 3;
  console.log(a); // 3
  console.log(global.a); // 2

})(window);

console.log(a); // 2
复制代码

得益于包装在一个 () 中,我们有了一个作为表达式的函数,我们可以通过在末尾加入另一个 () 来执行这个函数,就像 (function foo(){ .. })()。第一个外围的 ( ) 使这个函数变成表达式,而第二个 () 执行这个函数。

另一种(匿名函数表达式):

setTimeout(function () {
  console.log("I waited 1 second!");
}, 1000);
复制代码

有几个缺点需要考虑:

  1. 在栈轨迹上匿名函数没有有用的名称可以表示,这可能会使得调试更加困难。
  2. 没有名称的情况下,如果这个函数需要为了递归等目的引用它自己,那么就需要很不幸地使用 被废弃的arguments.callee 引用。另一个需要自引用的例子是,当一个事件处理器函数在被触发后想要把自己解除绑定。
  3. 匿名函数省略的名称经常对提供更易读/易懂的代码很有帮助。一个描述性的名称可以帮助代码自解释。

2 块儿作为作用域

JavaScript 没有块儿作用域的能力。

考虑这个for循环:

for (var i = 0; i < 10; i++) {
  console.log(i);
}
复制代码

这里使用 var 时,我们在何处声明变量是无关紧要的,因为它们将总是属于外围作用域。这个代码段实质上为了代码风格的原因“假冒”了块儿作用域,并依赖于我们要管好自己,不要在这个作用域的其他地方意外地使用 i。比如上面无限循环的例子。

为什么要用仅将(或者至少是,仅 应当)在这个 for 循环中使用的变量 i 去污染一个函数的整个作用域呢?

幸运的是,ES6 改变了这种状态,并引入了一个新的关键字 let,作为另一种声明变量的方式伴随着 var

let 关键字将变量声明附着在它所在的任何块儿(通常是一个 { .. })的作用域中。换句话说,let 为它的变量声明隐含地劫持了任意块儿的作用域。

var foo = true;

if (foo) {
  let bar = foo * 2;
  bar = something(bar);
  console.log(bar);
}

console.log(bar); // ReferenceError
复制代码

我们可以在一个语句是合法文法的任何地方,通过简单地引入一个 { .. } 来为 let 创建一个任意的可以绑定的块儿。在这个例子中,我们在 if 语句内部制造了一个明确的块儿,在以后的重构中将整个块儿四处移动可能会更容易,而且不会影响外围的 if 语句的位置和语义。

注意:使用 let 做出的声明将 不会 在它们所出现的整个块儿的作用域中提升。

{
  console.log(bar); // ReferenceError!
  let bar = 2;
}
复制代码

复习

区分函数声明与表达式的最简单的方法是,这个语句中(不仅仅是一行,而是一个独立的语句)“function”一词的位置。如果“function”是这个语句中的第一个东西,那么它就是一个函数声明。否则,它就是一个函数表达式。

在 JavaScript 中函数是最常见的作用域单位。在另一个函数内部声明的变量和函数,实质上对任何外围“作用域”都是“隐藏的”,这是优秀软件的一个有意的设计原则。

但是函数绝不是唯一的作用域单位。块儿作用域指的是这样一种想法:变量和函数可以属于任意代码块儿(一般来说,就是任意的 { .. }),而不是仅属于外围的函数。

从 ES3 开始,try/catch 结构在 catch 子句上拥有块儿作用域。

在 ES6 中,引入了 let 关键字(var 关键字的表兄弟)允许在任意代码块中声明变量。if (..) { let a = 2; } 将会声明变量 a,而它实质上劫持了 if{ .. } 块儿的作用域,并将自己附着在这里。

虽然有些人对此深信不疑,但是块儿作用域不应当被认为是 var 函数作用域的一个彻头彻尾的替代品。两种机能是共存的,而且开发者们可以并且应当同时使用函数作用域和块儿作用域技术 —— 在它们各自可以产生更好,更易读/易维护代码的地方。

转载于:https://juejin.im/post/5ad5746a51882555867fea69

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值