当我们声明变量的时候其实是使用所有编程语言的基本功能之一,就是能够存储变量当中的值,并且能在之后对这个值进行访问或者修改。而这其中到底发生了什么呢?我们先从编译原理说起。
编译原理
在传统的编程语言中,一段代码一般在执行之前会经历三个步骤:分词/词法分析,解析/语法分析,代码生成
分词/词法分析:这个过程一般会把由字符串分解成有意义的代码块,这些代码块被称为词法单元。例如var a = 2;通常会被分解为:var, a, =, 2, ;。空格是否有效取决于这门语言。
解析/词法分析:这个过程会把词法单元流转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树。这个树被称为是 “抽象语法树”。
代码生成:将抽象代码树转换为可执行的过程被称为代码生成。这个过程与语言,目标平台等相关。简单来说就是某种方法将var a = 2;的抽象语法树转化为一组机器指令,用来创建一个叫做a的变量(包括包括分配内存),并将一个值储存在a中。
这只是传统的编程语言,而JavaScript引擎要复杂的多。这里只是宏观的介绍。
var a = 2;
当我们写了var a =2;之后需要引擎,编译器,作用域的共同参与。
引擎:从头到尾负责执行整个JavaScript程序的编译及执行过程。
编译器:负责语法分析及代码生成等脏活累活。
作用域:负责收集并维护由所有声明的标识符组成的一系列查找,并实施一套非常严格的规则,确定当前指向的代码块对这些标识符的访问权限。
当看见var a = 2;这段程序时,很可能认为这是一句声明,但引擎却不这么看,引擎认为这是两个不同的声明,一个由编译器在编译时处理,另一个则由引擎在运行时处理。
编译器会进行如下的处理:
1.遇到var a,编译器会去询问作用域是否存在一个变量叫做a存在于相同的作用域,如果有,则忽略次此声明,如果没有,则它会要求作用域在当前的作用域集合中声明一个新的变量,命名为a。
2.接下来编译器会为引擎生成运行时的代码,这些代码用来处理a = 2这个赋值操作。引擎运行时会首先询问作用域,在当前的作用域集合中是否存在一个叫做a的变量。如果有,则使用该变量并且把2赋值给它,如果没有,则引擎会继续查找该变量,如果再找不到则会举手抛出一个错误。
总结:变量的赋值操作会执行两个操作,首先编译器会在当前的作用域中去声明一个变量(如果当前作用域没有的话或者说之前没有声明过),然后在运行时引擎会在作用域中查找该变量,如果能够找到就会对它赋值。
引擎查找的过程
上面说过,在编译过程的第二段生成了代码来给引擎去执行,它会通过查找去找变量a是否声明过,查找过程由作用域进行协助,但是引擎是怎样查找的呢?
在引擎查找的过程中会进行一次叫做 LHS,另外一个叫做 RHS。大概读者可以猜到L 和 R的意思吧。他们分别代表的是左侧和右侧。换句话说,当变量出现在变量的左侧时进行LHS查找,出现在右侧的时候进行RHS查找。其中RHS查找就是简单的查找某个变量的值,而LHS查找则是试图找到变量的容器本身,从而对其赋值。
深入研究
考虑下面一段代码:
console.log(a);
其中,对a的引用是一个RHS引用,因为这里对a进行的只是一个简单的查找,并未对其赋值。
相应的:
a = 2;
这里对a的引用则是LHS引用,因为实际上我们不关心a的值是什么,只是想要为” = “这个赋值操作找一个目标。
LHS和RHS的含义是“赋值操作的左侧和右侧”,可以理解为:赋值操作的目标是谁(LHS),以及谁是赋值操作的源头(RHS)
看下面这个例子:
function foo(a){
console.log(a)//2
}
foo(2);
最后一行的foo(…)函数的调用需要对foo进行RHS引用,意味着要去找foo的值,并把值赋给foo,然后通过(…)去执行,所以foo最好是一个函数类型的值。还有就是上面的代码中有a = 2这个操作,就是把2当作函数的参数传递给foo的时候,2会被分配给参数a,而为了给a赋值则需要进行一次LHS查找。之后的console.log(a)则需要对a进行一次RHS查找,并将值赋值给console.log(…)(其实console.log(..)因为需要进行RHS查找,并且还要检查得到的值中是否有一个叫做log的方法)。
注意这里我们只讨论变量的声明和赋值,对于函数的声明赋值来说进行LHS和RHS查找并不适合
function foo(a){
var b = a;
return a + b;
}
var c = foo(2);
上面的代码中有几次RHS和LHS呢?请大家自己思考一下。
三次LHS,四次RHS。
最后说明
上面我们说过,当进行查找的时候需要在作用域中去查找,然而往往我们在写代码的时候会有作用域嵌套的情况,因此,在当前作用域中无法找到某个变量时,引擎就会在外层嵌套的作用域去找,直到找到该变量或者到达最外层(也就是全局作用域)。
function foo(a){
console.log(a+b);
}
var b = 2;
foo(2);
在上面的代码中,需要对b进行RHS查找,但是在foo中找不到b,所以就会去上一层的作用域里去查找。
遍历嵌套的作用域的规则很简单,初学者或者有编程基础的大概都了解,引擎会从当前的执行作用域去查找变量,如果找不到就向上一层继续查找,当抵达最外层的全局作用域的时候无论找到还是没找到,查找过程都会停止。
这个时候大家或许就会有疑问了,如果找不到怎么办???
查找的情况分为两种RHS和LHS查找,这两种查找中对找不到的结果处理也是不一样的。
function foo(a){
console.log(a+b);
b = a;
}
foo(2);
在变量还没有声明的时候进行不同的查找情况下,例如上面的代码中对b进行RHS查找,因为这是一个还没有声明的变量,所以在任何作用域中都是找不到的,如果RHS查找在所有的嵌套作用域里面都找不到,引擎就会抛出 ReferenceError异常,记住ReferenceError是非常重要的错误类型!!!。
相比之下如果进行LHS查找时,如果在全局作用域里面找不到目标变量,全局作用域就会创建一个具有该名称的变量,并将其返回给引擎,前提是程序运行在”非严格模式“下,在拿到这个变量之后如果对它进行不合理的操作时,比如对一个非函数类型的值进行函数调用时,引擎会抛出TypeError错误。
以上内容出自《你不知道的JavaScript 》上卷,第一章,内容为自己总结,也用到了书中的一些例子。
如有错误,还请指出,会及时更改