1.理解JavaScript中的LHS 和 RHS 查询。
JavaScript中在预编译后执行代码时对变量的查询分为LHS(Left-Hand-Side)查询和RHS(Right-Hand-Side)查询。
当你看到 var a = 2; 这段程序的时候,很可能认为这是一句声明,但是浏览器的引擎并不这么看,事实上引擎认为这里有两个完全不同的声明,一个由编译器在编译时候处理,另一个则是在引擎运行时处理。
下面我们将 var a = 2
; 分解,看看引擎和它的朋友们是如何协同工作的。
编译器首先会将这段程序分解成词法单元,然后将词法单元解析成一个树结构,但是当编译器开始进行代码生成的时候,它对这段程序的处理方式会和逾期的有所不同。
可以合理的假设编译器所产生的代码能够用下面的伪代码进行概括:“为一个变量分配内存,将其命名为a,然后将值2保存进这个变量”然而,这并不完全正确。
事实上编译器会进行如下的处理:
1.遇到 var a,编译器会询问作用域是否已经有一个该名称的变量存在于同一个作用域的集合中,如果是编译器会自动忽略该声明继续进行编译,否则它会要求作用域在当前作用域的集合中声明一个新的变量,并命名为a。
2.接下来编译器会为引擎生成运行时所需要的代码,这些代码被用来处理 a=2 这个赋值操作引擎运行时会首先询问作用域在当前的作用域集合中是否存在一个叫做a的变量。如果是,引擎就会使用这个变量;如果不是,引擎会继续查找该变量
如果引擎最终找到了a变量,就会将2赋值给它,否则引擎就会举手示意抛出一个异常
总结:变量的赋值操作会执行两个动作,首先编译器会在当前作用域中声明一个变量,然后再运行时引擎会在作用域中查找该变量,如果能够找到就会对它进行赋值。
为了进一步理解,我们需要多介绍一些编译器的术语。
编译器在编译过程中的第二步中生成代码,引擎执行它时,会通过查找变量a来判断它时否已经声明过,查找的过程由
当变量出现在赋值操作的左侧时进行 LHS 查询,出现在右侧时进行 RHS 查询。
讲个更加清楚一些,RHS查询与简单地查找某一个变量的值别无二致,而LHS查询则是试图找到变量本身容器本身,从而可以对其赋值,从这个角度来说,RHS并不是真正意义上的“赋值操作的右侧”,更准确地说是“非左侧”。
你可以将RHS理解成 retrueve his source value,这意味着“得到某某的值”
让我们继续深入研究。
考虑以下代码:
onsole.log(a);
其中对 a 的引用是一个RHS引用,因为这里a并没有赋予任何值。相应地,需要查找并取得a的值,这样才能将值传递给console.log(...)
。
相比之下,例如:
a = 2;
这里对ade 引用则是LHS的引用,因为实际上我们并不关心当前的值是什么,知识想要为 = 2 这个赋值操作找到一个目标。
考虑下面的程序,其中既有LHS也有RHS引用:
function(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的方法。
2.作用域的嵌套
我们说过,作用域是根据名称查找变量的一套规则,实际情况中,通常需要同时顾忌几个作用域。
当一个块或者函数嵌套在另一个块或者函数中的时候,就发生了作用域的嵌套,因此,在当前作用域中无法找到某一个变量时候,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量,或者抵达最外层的作用域(也就是全局的作用域)为止。
考虑一下代码:
function foo(a) {
console.log(a+b);
}
var b = 2;
foo( 2 ); // 4s
对b进行的 RHS 引用无法在 函数 foo 内部完成,但是可以在上一级作用域(在这个例子中就是全局作用域)中完成。
遍历嵌套作用域链的规则很简单:引擎从当前的执行作用域开始查找变量,如果找不到就向上一级继续查找,当抵达最外层的全局作用域时候,无论找到还是没有找到 查找过程都会停止。
3.区分LHS和RHS是一件重要的事情
在变量还没有声明(在任何的作用域中都无法找到该变量)的情况下,这两种查询的行为是不一样的。
考虑如下代码:
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 则代表作用域的判断成功了,但是对结果的操作是非法或者不合理的。
4.总结一下
作用域是一套规则,用于确定在何处以及如何查找变量(标识符)如果查找的目的是对变量进行赋值,那么就会使用LHS查询,如果目的是获取变量的值,就会使用RHS进行查询。
=操作符号或者调用函数时候传入的参数操作都会导致关联作用域的赋值操作。
Javascript 引擎首先会在代码执行前对其进行编译,在这个过程中,像 var a = 2 这样的声明会被分解为两个独立的步骤:
1.首先 var a 在其作用域中声明新的变量,这会在最开始的阶段,也就是在代码执行前进行。
2.接下来 a = 2 会在查询(LHS)变量a并对其进行赋值。
LHS和RHS查询都会从当前的作用域中开始,如果有需要,就会向上级作用域继续查找目标,这样的层级查找直到抵达全局的作用域,无论找到或没有找到都会停止。
不成功的 RHS 引用会导致抛出 ReferenceError 异常。不成功的 LHS 引用会导致自动隐式 地创建一个全局变量(非严格模式下),该变量使用 LHS 引用的目标作为标识符,或者抛 出 ReferenceError 异常(严格模式下)。
5.巩固知识
通过解析下面这段代码复习一遍刚才理解和总结的LHS和RHS的概念。
function foo(a) {
var b = a;
return a + b;
}
var c = foo( 2 );
LHS: 3处
1.首先在执行 var c = foo(2) 这段代码的时候 变量 c 的赋值就是一个 LHS操作
2.这里foo函数执行了一个传递参数的行为,上文提到这种行为属于隐式类型分配--> a = 2 属于LHS
3.执行到 foo函数内部 var b = a; 给b赋值的操作是一个LHS行为
RHS:4处
1.var c = foo(2) 这段代码 中对于 foo函数的执行就是一个RHS的查询操作。
2.执行到 foo函数内部 var b = a; 对a 变量的查找的行为就是一个RHS查询操作。
3.return a + b; 这段代码 执行的时候 分别查询了 a 和 b
参考资料 《你不知道的JavaScript》上卷