变量的赋值操作会执行两个动作, 首先编译器会在当前作用域中声明一个变量(如果之前没有声明过), 然后在运行时引擎会在作用域中查找该变量, 如果能够找到就会对它赋值。----《你所不知道的JavaScript(上)》 P7
而要讲的 LHS 和 RHS 就是上面说的对变量的两种查找操作,查找的过程是由作用域(词法作用域)进行协助,但是引擎执行怎样的查找, 会影响最终的查找结果。
1、LHS(Left Hand Side)和 RHS(Right Hand Side)
当变量出现在赋值操作的左侧时进行 LHS 查询, 出现在右侧时进行 RHS 查询。讲得更准确一点, RHS 查询与简单地查找某个变量的值别无二致, 而 LHS 查询则是试图找到变量的容器本身, 从而可以对其赋值。 从这个角度说, RHS 并不是真正意义上的“赋值操作的右侧”, 更准确地说是“非左侧”。 ----《你所不知道的JavaScript(上)》 P7
简单来说,
(1)LHS查询指的是找到变量的容器本身,从而可以对其进行赋值。也就是找到赋值操作的目标。LHS查询的时候会沿着作用域链进行查询,找到的话就会将值赋值给这个变量,如果到达作用域顶端仍然找不到,就会在作用域链顶端创建这个变量(在严格模式中 LHS 查询失败时, 并不会创建并返回一个全局变量, 引擎会抛出同 RHS 查询失败时类似的 ReferenceError 异常) 。
var a = 2;
这里对 a 的引用则是 LHS 引用, 因为实际上我们并不关心当前a的值是什么, 只是想要为 =2 这个赋值操作找到一个目标。
(2)RHS查询就是普通的查询变量的值,即获取变量的值。RHS查询的时候会沿着作用域链进行查询,找到的话就会取得这个值并返回,如果到达作用域顶端仍然找不到,引擎就会抛出 ReferenceError异常。如果 RHS 查询找到了一个变量, 但是你尝试对这个变量的值进行不合理的操作,比如试图对一个非函数类型的值进行函数调用, 或着引用 null 或 undefined 类型的值中属性, 那么引擎会抛出另外一种类型的异常, 叫作 TypeError。
(注:ReferenceError 同作用域判别失败相关, 而 TypeError 则代表作用域判别成功了, 但是对结果的操作是非法或不合理的。)
举个栗子:
console.log(a);
这里的a就是一个RHS引用,因为console.log需要获取到a的值才能输出a的值。当然这里的console.log也是一个RHS引用,这里对console 对象进行RHS 查询,并且检查得到的值中是否有一个叫作log 的方法。例子中的a因为没有声明过,所以会抛出错误。如下图所示:
2、实例详解
实例1:
function foo(a) { console.log( a ); } foo( 2 );
(1)foo(..) 函数的调用需要对 foo 进行RHS引用 ,意思是“去找到 foo 的值, 并把它给我 ”。
(2)这里还有一个容易被忽略却非常重要的细节。代码中隐式的 a=2 操作可能很容易被你忽略掉。这个操作发生在 2 被当作参数传递给foo(..) 函数时,2 会被分配给参数a。为了给参数a(隐式地)分配值,需要进行一次LHS 查询。
(3)console.log(a)这里还有对a进行的RHS引用,并且将得到的值传给了console.log(..)。console.log(..) 本身也需要一个引用才能执行, 因此会对console对象进行RHS查询,并且检查得到的值中是否有一个叫作log的方法。
所以这里一共进行了1次LHS查询3次RHS查询。
让我们把上面这段代码的处理过程想象成一段对话, 这段对话可能是下面这样的。
引擎: 我说作用域, 我需要为 foo 进行 RHS 引用。 你见过它吗?
作用域: 别说, 我还真见过, 编译器那小子刚刚声明了它。 它是一个函数, 给你。
引擎: 哥们太够意思了! 好吧, 我来执行一下 foo。
引擎: 作用域, 还有个事儿。 我需要为 a 进行 LHS 引用, 这个你见过吗?
作用域: 这个也见过, 编译器最近把它声名为 foo 的一个形式参数了, 拿去吧。
引擎: 大恩不言谢, 你总是这么棒。 现在我要把 2 赋值给 a。
引擎: 哥们, 不好意思又来打扰你。 我要为 console 进行 RHS 引用, 你见过它吗?
作用域: 咱俩谁跟谁啊, 再说我就是干这个。 这个我也有, console 是个内置对象。给你。
引擎: 么么哒。 我得看看这里面是不是有 log(..)。 太好了, 找到了, 是一个函数。
引擎: 哥们, 能帮我再找一下对 a 的 RHS 引用吗? 虽然我记得它, 但想再确认一次。
作用域: 放心吧, 这个变量没有变动过, 拿走, 不谢。
引擎: 真棒。 我来把 a 的值, 也就是 2, 传递进 log(..)。
----《你所不知道的JavaScript(上)》 P9
实例2:
function foo(a) { var b = a; return a + b; } var c = foo( 2 );
以上代码中有3个LHS与4个RHS,分析如下:
(1)var c中的c需要被赋值,在赋值操作的左侧,所以对c进行LHS引用。
(2)变量c需要被赋值,他的值是foo(2),那么foo(2)的值是多少呢,需要查找foo(2)的值,在赋值操作的右侧,所以对foo(2)进行了一次RHS查询。
(3)隐含赋值操作,将2传递给function foo(a){……}函数的参数a,a=2,a在赋值操作的左侧,对a进行了一次LHS查询。
(4)var b=a;中,b需要被赋值,处在赋值操作的左侧,所以对b进行了一次LHS查询,b的值将从a来,那么右侧的a的值从何而来呢?这就需要对赋值操作右侧的a进行了一次RHS查询。
(5)return a+b;中,需要找到a与b的值的来源,a与b都在赋值操作的右侧,才能得到a+b的值,所以对a与b都是进行一次RHS查询。
注:console.log(..) 本身也需要一个引用才能执行,因此会对console 对象进行RHS 查询,并且检查得到的值中是否有一个叫作log 的方法。这里不会再对log进行RHS查询。因为对console查询完毕后,对象属性访问规则会接管对log属性的访问。也就是说,如果是访问对象的属性就不存在LHS查询和RHS查询了,找不到就返回undefined。
3、小结:
如果查找的目的是对变量进行赋值, 那么就会使用 LHS 查询; 如果目的是获取变量的值, 就会使用 RHS 查询。赋值操作符会导致 LHS 查询。 =操作符或调用函数时传入参数的操作都会导致关联作用域的赋值操作。JavaScript 引擎首先会在代码执行前对其进行编译, 在这个过程中, 像 var a = 2 这样的声明会被分解成两个独立的步骤:
1. 首先, var a 在其作用域中声明新变量。 这会在最开始的阶段, 也就是代码执行前进行。
2. 接下来, a = 2 会查询(LHS 查询) 变量 a 并对其进行赋值。
LHS 和 RHS 查询都会在当前执行作用域中开始, 如果有需要(也就是说它们没有找到所需的标识符), 就会向上级作用域继续查找目标标识符, 这样每次上升一级作用域, 最后抵达全局作用域, 无论找到或没找到都将停止。不成功的 RHS 引用会导致抛出 ReferenceError 异常。 不成功的 LHS 引用会导致自动隐式地创建一个全局变量(非严格模式下), 该变量使用 LHS 引用的目标作为标识符, 或者抛出 ReferenceError 异常(严格模式下)。