文章目录
  1. 1. 理解作用域
    1. 1.1. RHS LHS
    2. 1.2. 引擎和作用域的对话
  2. 2. 作用域嵌套
  3. 3. 异常
  4. 4. 小结

理解作用域

RHS LHS

它们分别代表左侧和右侧。
什么东西的左侧和右侧?是一个赋值操作的左侧和右侧。
换句话说,当变量出现在赋值操作的左侧时进行LHS查询,出现在右侧时进行RHS查询。
讲得更准确一点,RHS查询与简单地查找某个变量的值别无二致,而LHS查询则是试图找到变量
的容器本身,从而可以对其赋值。从这个角度说,RHS并不是真正意义上的“赋值操作的右侧”,更准
确地说是“非左侧”。
你可以将RHS理解成retrieve his source value(取到它的源值),这意味着“得到某某的值”。

考虑以下代码:

1
console.log( a );

其中对a的引用是一个RHS引用,因为这里a并没有赋予任何值。相应地,需要查找并取得a的值,这
样才能将值传递给console.log(..)。
相比之下,例如:

1
a = 2;

这里对a的引用则是LHS引用,因为实际上我们并不关心当前的值是什么,只是想要为= 2这个赋
值操作找到一个目标。

LHS和RHS的含义是“赋值操作的左侧或右侧”并不一定意味着就是“=赋值操作符的左侧
或右侧”。赋值操作还有其他几种形式,因此在概念上最好将其理解为“赋值操作的目标是谁
(LHS)”以及“谁是赋值操作的源头(RHS)”。

考虑下面的程序,其中既有LHS也有RHS引用:

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

最后一行foo(..)函数的调用需要对foo进行RHS引用,意味着“去找到foo的值,并把它给我”。并
且(..)意味着foo的值需要被执行,因此它最好真的是一个函数类型的值!
这里还有一个容易被忽略却非常重要的细节。
代码中隐式的a = 2操作可能很容易被你忽略掉。这个操作发生在2被当作参数传递给foo(..)函数
时,2会被分配给参数a。为了给参数a(隐式地)分配值,需要进行一次LHS查询。
这里还有对a进行的RHS引用,并且将得到的值传给了console.log(..)。console.log(..)本身也需
要一个引用才能执行,因此会对console对象进行RHS查询,并且检查得到的值中是否有一个叫
作log的方法。
最后,在概念上可以理解为在LHS和RHS之间通过对值2进行交互来将其传递进log(..)(通过变
量a的RHS查询)。假设在log(..)函数的原生实现中它可以接受参数,在将2赋值给其中第一个(也
许叫作arg1)参数之前,这个参数需要进行LHS引用查询。

你可能会倾向于将函数声明function foo(a) {…概念化为普通的变量声明和赋值,比
如var foo、foo = function(a) {…。如果这样理解的话,这个函数声明将需要进行LHS查询。
然而还有一个重要的细微差别,编译器可以在代码生成的同时处理声明和值的定义,比如在引
擎执行代码时,并不会有线程专门用来将一个函数值“分配给”foo。因此,将函数声明理解成前
面讨论的LHS查询和赋值的形式并不合适。

引擎和作用域的对话

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

让我们把上面这段代码的处理过程想象成一段对话,这段对话可能是下面这样的。

引擎:我说作用域,我需要为foo进行RHS引用。你见过它吗?
作用域:别说,我还真见过,编译器那小子刚刚声明了它。它是一个函数,给你。
引擎:哥们太够意思了!好吧,我来执行一下foo。
引擎:作用域,还有个事儿。我需要为a进行LHS引用,这个你见过吗?
作用域:这个也见过,编译器最近把它声名为foo的一个形式参数了,拿去吧。
引擎:大恩不言谢,你总是这么棒。现在我要把2赋值给a。
引擎:哥们,不好意思又来打扰你。我要为console进行RHS引用,你见过它吗?
作用域:咱俩谁跟谁啊,再说我就是干这个的。这个我也有,console是个内置对象。给你。
引擎:么么哒。我得看看这里面是不是有log(..)。太好了,找到了,是一个函数。
引擎:哥们,能帮我再找一下对a的RHS引用吗?虽然我记得它,但想再确认一次。
作用域:放心吧,这个变量没有变动过,拿走,不谢。
引擎:真棒。我来把a的值,也就是2,传递进log(..)。
……

作用域嵌套

作用域是根据名称查找变量的一套规则。实际情况中,通常需要同时顾及几个作用域。
当一个块或函数嵌套在另一个块或函数中时,就发生了作用域的嵌套。因此,在当前作用域中无法
找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量,或抵达最外层的作
用域(也就是全局作用域)为止。

考虑以下代码:

1
2
3
4
5
function foo(a) {
console.log( a + b );
}
var b = 2;
foo( 2 ); // 4

对b进行的RHS引用无法在函数foo内部完成,但可以在上一级作用域(在这个例子中就是全局作
用域)中完成。
因此,回顾一下引擎和作用域之间的对话,会进一步听到:

引擎:foo的作用域兄弟,你见过b吗?我需要对它进行RHS引用。
作用域:听都没听过,走开。
引擎:foo的上级作用域兄弟,咦?有眼不识泰山,原来你是全局作用域大哥,太好了。你见过b
吗?我需要对它进行RHS引用。
作用域:当然了,给你吧。

遍历嵌套作用域链的规则很简单:引擎从当前的执行作用域开始查找变量,如果找不到,就向上一
级继续查找。当抵达最外层的全局作用域时,无论找到还是没找到,查找过程都会停止。

异常

为什么区分LHS和RHS是一件重要的事情?
因为在变量还没有声明(在任何作用域中都无法找到该变量)的情况下,这两种查询的行为是不一
样的。

考虑如下代码:

1
2
3
4
5
function foo(a) {
console.log( a + b );
b = a;
}
foo( 2 );

第一次对b进行RHS查询时是无法找到该变量的。也就是说,这是一个“未声明”的变量,因为在任何
相关的作用域中都无法找到它。
如果RHS查询在所有嵌套的作用域中遍寻不到所需的变量,引擎就会抛出ReferenceError异常。值
得注意的是,ReferenceError是非常重要的异常类型。
相较之下,当引擎执行LHS查询时,如果在顶层(全局作用域)中也无法找到目标变量,全局作用域
中就会创建一个具有该名称的变量,并将其返还给引擎,前提是程序运行在非“严格模式”下。
“不,这个变量之前并不存在,但是我很热心地帮你创建了一个。”
ES5中引入了“严格模式”。同正常模式,或者说宽松/懒惰模式相比,严格模式在行为上有很多不
同。其中一个不同的行为是严格模式禁止自动或隐式地创建全局变量。因此,在严格模式中LHS查
询失败时,并不会创建并返回一个全局变量,引擎会抛出同RHS查询失败时类似的ReferenceError
异常。
接下来,如果RHS查询找到了一个变量,但是你尝试对这个变量的值进行不合理的操作,比如试图
对一个非函数类型的值进行函数调用,或着引用null或undefined类型的值中的属性,那么引擎会
抛出另外一种类型的异常,叫作TypeError。
ReferenceError同作用域判别失败相关,而TypeError则代表作用域判别成功了,但是对结果的操作
是非法或不合理的。

小结

作用域是一套规则,用于确定在何处以及如何查找变量(标识符)。如果查找的目的是对变量进行
赋值,那么就会使用LHS查询;如果目的是获取变量的值,就会使用RHS查询。
赋值操作符会导致LHS查询。=操作符或调用函数时传入参数的操作都会导致关联作用域的赋值
操作。
JavaScript引擎首先会在代码执行前对其进行编译,在这个过程中,像var a = 2这样的声明会被分
解成两个独立的步骤:

  1. 首先,var a在其作用域中声明新变量。这会在最开始的阶段,也就是代码执行前进行。
  2. 接下来,a = 2会查询(LHS查询)变量a并对其进行赋值。
    LHS和RHS查询都会在当前执行作用域中开始,如果有需要(也就是说它们没有找到所需的标识
    符),就会向上级作用域继续查找目标标识符,这样每次上升一级作用域(一层楼),最后抵达全局
    作用域(顶层),无论找到或没找到都将停止。
    不成功的RHS引用会导致抛出ReferenceError异常。不成功的LHS引用会导致自动隐式地创建一个
    全局变量(非严格模式下),该变量使用LHS引用的目标作为标识符,或者抛出ReferenceError异常
    (严格模式下)。
文章目录
  1. 1. 理解作用域
    1. 1.1. RHS LHS
    2. 1.2. 引擎和作用域的对话
  2. 2. 作用域嵌套
  3. 3. 异常
  4. 4. 小结