理解作用域

我们将采用的学习作用域的方法,是将这个处理过程想象为一场对话。但是, 在进行这场对话呢?

演员

让我们见一见处理程序 var a = 2; 时进行互动的演员吧,这样我们就能理解稍后将要听到的它们的对话:

  1. 引擎:负责从始至终的编译和执行我们的 JavaScript 程序。

  2. 编译器引擎 的朋友之一;处理所有的解析和代码生成的重活儿(见前一节)。

  3. 作用域引擎 的另一个朋友;收集并维护一张所有被声明的标识符(变量)的列表,并对当前执行中的代码如何访问这些变量强制实施一组严格的规则。

为了 全面理解 JavaScript 是如何工作的,你需要开始像 引擎(和它的朋友们)那样 思考,问它们问的问题,并像它们一样回答。

反复

当你看到程序 var a = 2; 时,你很可能认为它是一个语句。但这不是我们的新朋友 引擎 所看到的。事实上,引擎 看到两个不同的语句,一个是 编译器 将在编译期间处理的,一个是 引擎 将在执行期间处理的。

那么,让我们来分析 引擎 和它的朋友们将如何处理程序 var a = 2;

编译器 将对这个程序做的第一件事情,是进行词法分析来将它分解为一系列 token,然后这些 token 被解析为一棵树。但是当 编译器 到了代码生成阶段时,它会以一种与我们可能想象的不同的方式来对待这段程序。

一个合理的假设是,编译器 将产生的代码可以用这种假想代码概括:“为一个变量分配内存,将它标记为 a,然后将值 2 贴在这个变量里”。不幸的是,这不是十分准确。

编译器 将会这样处理:

  1. 遇到 var a编译器作用域 去查看对于这个特定的作用域集合,变量 a 是否已经存在了。如果是,编译器 就忽略这个声明并继续前进。否则,编译器 就让 作用域 去为这个作用域集合声明一个称为 a 的新变量。

  2. 然后 编译器引擎 生成稍后要执行的代码,来处理赋值 a = 2引擎 运行的代码首先让 作用域 去查看在当前的作用域集合中是否有一个称为 a 的变量可以访问。如果有,引擎 就使用这个变量。如果没有,引擎 就查看 其他地方(参见下面的嵌套 作用域 一节)。

如果 引擎 最终找到一个变量,它就将值 2 赋予它。如果没有,引擎 将会举起它的手并喊出一个错误!

总结来说:对于一个变量赋值,发生了两个不同的动作:第一,编译器 声明一个变量(如果先前没有在当前作用域中声明过),第二,当执行时,引擎作用域 中查询这个变量并给它赋值,如果找到的话。

编译器术语

为了继续更深入地理解,我们需要一点儿更多的编译器术语。

引擎 执行 编译器 在第二步为它产生的代码时,它必须查询变量 a 来看它是否已经被声明过了,而且这个查询是咨询 作用域 的。但是 引擎 所实施的查询的类型会影响查询的结果。

在我们这个例子中,引擎 将会对变量 a 实施一个“LHS”查询。另一种类型的查询称为“RHS”。

我打赌你能猜出“L”和“R”是什么意思。这两个术语表示“Left-hand Side(左手边)”和“Right-hand Side(右手边)”

什么的……边?赋值操作的。

换言之,当一个变量出现在赋值操作的左手边时,会进行 LHS 查询,当一个变量出现在赋值操作的右手边时,会进行 RHS 查询。

实际上,我们可以表述得更准确一点儿。对于我们的目的来说,一个 RHS 是难以察觉的,因为它简单地查询某个变量的值,而 LHS 查询是试着找到变量容器本身,以便它可以赋值。从这种意义上说,RHS 的含义实质上不是 真正的 “一个赋值的右手边”,更准确地说,它只是意味着“不是左手边”。

在这一番油腔滑调之后,你也可以认为“RHS”意味着“取得他/她的源(值)”,暗示着 RHS 的意思是“去取……的值”。

让我们挖掘得更深一些。

当我说:

  1. console.log( a );

这个指向 a 的引用是一个 RHS 引用,因为这里没有东西被赋值给 a。而是我们在查询 a 并取得它的值,这样这个值可以被传递进 console.log(..)

作为对比:

  1. a = 2;

这里指向 a 的引用是一个 LHS 引用,因为我们实际上不关心当前的值是什么,我们只是想找到这个变量,将它作为 = 2 赋值操作的目标。

注意: LHS 和 RHS 意味着“赋值的左/右手边”未必像字面上那样意味着“ = 赋值操作符的左/右边”。赋值有几种其他的发生形式,所以最好在概念上将它考虑为:“赋值的目标(LHS)”和“赋值的源(RHS)”。

考虑这段程序,它既有 LHS 引用又有 RHS 引用:

  1. function foo(a) {
  2. console.log( a ); // 2
  3. }
  4. foo( 2 );

调用 foo(..) 的最后一行作为一个函数调用要求一个指向 foo 的 RHS 引用,意味着,“去查询 foo 的值,并把它交给我”。另外,(..) 意味着 foo 的值应当被执行,所以它最好实际上是一个函数!

这里有一个微妙但重要的赋值。你发现了吗?

你可能错过了这个代码段隐含的 a = 2。它发生在当值 2 作为参数值传递给 foo(..) 函数时,值 2 被赋值 给了参数 a。为了(隐含地)给参数 a 赋值,进行了一个 LHS 查询。

这里还有一个 a 的值的 RHS 引用,它的结果值被传入 console.log(..)console.log(..) 需要一个引用来执行。它为 console 对象进行一个 RHS 查询,然后发生一个属性解析来看它是否拥有一个称为 log 的方法。

最后,我们可以将这一过程概念化为,在将值 2(通过变量 a 的 RHS 查询得到的)传入 log(..) 时发生了一次 LHS/RHS 的交换。在 log(..) 的原生实现内部,我们可以假定它拥有参数,其中的第一个(也许被称为 arg1)在 2 被赋值给它之前,进行了一次 LHS 引用查询。

注意: 你可能会试图将函数声明 function foo(a) {... 概念化为一个普通的变量声明和赋值,比如 var foofoo = function(a){...。这样做会诱使你认为函数声明涉及了一次 LHS 查询。

然而,一个微妙但重要的不同是,在这种情况下 编译器 在代码生成期间同时处理声明和值的定义,如此当 引擎 执行代码时,没有必要将一个函数值“赋予” foo。因此,将函数声明考虑为一个我们在这里讨论的 LHS 查询赋值是不太合适的。

引擎/作用域对话

  1. function foo(a) {
  2. console.log( a ); // 2
  3. }
  4. foo( 2 );

让我们将上面的(处理这个代码段的)交互想象为一场对话。这场对话将会有点儿像这样进行:

引擎:嘿 作用域,我有一个 foo 的 RHS 引用。听说过它吗?

作用域;啊,是的,听说过。编译器 刚在一秒钟之前声明了它。它是一个函数。给你。

引擎:太棒了,谢谢!好的,我要执行 foo 了。

引擎:嘿,作用域,我得到了一个 a 的 LHS 引用,听说过它吗?

作用域:啊,是的,听说过。编译器 刚才将它声明为 foo 的一个正式参数了。给你。

引擎:一如既往的给力,作用域。再次感谢你。现在,该把 2 赋值给 a 了。

引擎:嘿,作用域,很抱歉又一次打扰你。我需要 RHS 查询 console。听说过它吗?

作用域:没关系,引擎,这是我一天到晚的工作。是的,我得到 console 了。它是一个内建对象。给你。

引擎:完美。查找 log(..)。好的,很好,它是一个函数。

引擎:嘿,作用域。你能帮我查一下 a 的 RHS 引用吗?我想我记得它,但只是想再次确认一下。

作用域:你是对的,引擎。同一个家伙,没变。给你。

引擎:酷。传递 a 的值,也就是 2,给 log(..)

小测验

检查你到目前为止的理解。确保你扮演 引擎,并与 作用域 “对话”:

  1. function foo(a) {
  2. var b = a;
  3. return a + b;
  4. }
  5. var c = foo( 2 );
  1. 找到所有的 LHS 查询(有3处!)。

  2. 找到所有的 RHS 查询(有4处!)。

注意: 小测验答案参见本章的复习部分!