引擎与作用域及编译器
在传统的编译语言的流程中,程序的一段源代码主要分成三步,统称为“编译”
- 分词/词法分析
它的主要作用是将字符组成的字符串分解成有意义的代码块,例如:var a=2;者会被分解成“var”,“a”,“=”,“2”。
空格是否被分解主要是看空格在该编程语言中有没有意义。
- 解析/语法分析
这个过程是将词法单元流(数组)转换成由元素逐级嵌套所组成的程序语法结构的树。这棵树被称为“抽象语法树”
- 代码生成
将AST转换成可执行代码的过程。如果不考虑具体细节的话,那么简单来说此过程便是将var a=2这一组AST转换成机器指令,用来创建一个叫做a的变量(分配内存),将2存储在a中。
Java Script引擎要比这个过程复杂得多,但是它没有那么多时间来进行优化或者对代码的其他处理,因为Java Script代码通常在代码执行前的几微秒内进行编译和处理,看到这,也许你会有疑惑,为什么Java Script引擎能这么快呢?这是因为在我们讨论的作用域背后Java Script用尽了各种办法,将性能达到最优化。
作用域
要想了解作用域是什么?起到怎么样的作用,必须还得了解以下的概念。
- 引擎
从头到尾负责代码的编译与运行
- 编译器
负责语法分析与代码生成
- 作用域
负责收集和维护所有声明的标识符组成的一系列查询,并实施一套非常严格的规则,判断当前执行的代码对变量访问的权限。通俗点来讲就像是仓库的管理员,它负责物品(声明的标识符)的安全(维护),以及的上架(收集)。对来访者(引擎或者编译器)取物品进行检查判断(判断权限)。
若你还是觉得很难理解,那没有关系,我们举个例子
**例Java Script执行代码“ var a=2”。当引擎见到此声明时,它不仅会自己处理,还会将此声明抛给编译器进行处理。**
**编译器的处理**:将“var a=2”分解为词法单元,由这些词法单元生成抽象语法树。然后接着生成代码,那么由伪代码描述“var a=2”,它的执行结果是这样的:“为一个变量分配内存,并命名为a,然后将2这个值保存在变量里面”。尽管看上去很完美,但是由于作用域的存在,编译器不得不进行另一种操作。
遇到“var a=2”,编译器会先向作用域询问是否有一个该名称的变量存在于作用域中,是的话它就会忽略这次声明,继续编译。否的话,它就会要求作用域在当前作用域集合中创建一个名为"a"的变量,然后再编译。我们来按照之前的仓库理解,编译器是对商品(代码)加工的工人,当它遇到“var a=2”的物品清单(声明),它会将这个物品罗列出来(词法分析),然后看看物品之间的需求需要(抽象语法树),接着去仓库找物品(生成代码),在仓库的门口遇到了仓库管理员(当前的作用域集合),提出了物品要求,仓库管理员进库检索物品(查看是否创建了名为"a"的变量),若有,那么将物品递给工人(忽略此次声明);没有的话,则向总部提出此库("当前的作用域结合")物品进货(创建"a"变量)
接下来编译器将为引擎运生成运行的代码,这些代码用来处理a=2这个赋值操作。引擎运行时会首先询问作用域,当前作用域中是否存在"a"这个变量,是的话,继续操作,不是的话,将在作用域中急促查找。
引擎最终找到了"a"这个变量,便会将2赋值给它,不然的话,它就会抛出一个异常。
总结:在对"var a=2"的执行过程中,编译器首先对代码进行词法分析、语法分析,然后创建变量(如果作用域中不存在的话),运行时,引擎再一次在作用域中查询变量,如果找到的话就给它赋值。(经历了两次变量查询,此处的作用域指的是当前的作用域集合)
引擎查询变量的方式
在编译器将声明生成可运行代码时,我们的引擎将会进行变量查找,引擎进行怎样的查找将会影响到查找的结果。
在Java Script中引擎查找变量涉及到的查找方式主要有两种,LHS查询以及RHS查询,**LHS是在赋值操作左侧查找**,**RHS是赋值操作右侧查找**,在我们的例子中,“var a=2”涉及到的是LHS查询。
例:
console.log(a);
在这里面就涉及到了RHS查询,怎么理解呢?在这代码里面"a"没有值,相应的它需要查找得到"a"的值,这样的话才能把值传给console.log();
我们把两个例子进行比较
a=2
在这里面我们并不需要查找"a"的值,而是要为"=2"这一个操作找到一个目标。
为了更加熟悉"LHS"以及"RHS",我们再举个例子
-------------------------------------------
function foo(a){
console.log(a);
};
foo(2);
---------------------------------------------
首先我们先看看foo()函数,console.log(a);中涉及到了"RHS"查询。
在foo(2)中我们也涉及到了"RHS"查询(查找foo()),但是,别忽略了,在此函数中将"2"的值传给形参"a"即"a=2"此处涉及到了"LHS"查询。
这里面涉及到了几次"RHS"以及"LHS"查询呢?
foo(2)------一次"RHS'查询,查询foo()函数,
console.log(a)--------三次"RHS"查询,一次查询console,一次.log,一次"a"
在传递形参时-------一次"LHS"查询
总结:"LHS"是找到操作的容器,"RHS"是查找变量的值
那么为什么要区分LHS和RHS,这对我们调试Java Script是有好处的
- 当作用域进行RHS查询时,查询不到结果时,它便会抛出ReferncError错误,而在查询到变量,但是对变量的引用不正确的情况下,会抛出TypeError错误。
- 当作用域进行LHS查询时,在非ES5的严格模式下,它查询不到变量时,便会主动创建一个变量。而在ES5的严格模式下,它则会弹出ReferenceError错误。
作用域嵌套
在我们举得例子中,如编译器对变量的查询,引擎对变量的查询都是在当前作用域集合中操作的,通常不仅只有这么一个作用域,而是多个。
例如,当一个块或者函数在另一个块或者函数中时,便会发生作用域的嵌套,首先它们会在当前作用域下查询变量,没有查询到则返回外层作用域(直到最外层全局作用域)--------------这就是作用域的嵌套。
举个例子:
function foo(a){
console.log(a+b);
}
var b=2;
foo(2);
在foo函数中我们需要对b进行RHS引用(查询b),但是在foo()函数这一层的作用域中,并没有查询到b,于是它便回到外层作用域查询b,在"var b=2"中查询到了b。
运用我们之前讲过的仓库例子,引擎是一名采购商人,它需要b这一个商品(RHS引用),于是去问了当前商店所在的仓库管理员(当前作用域),“这里有b这个商品吗?”,“没有,但是您可以去更高级别的仓库(外层作用域)找找”仓库管理员回答,于是采购商便去更高级别的仓库找到了b。
嵌套规则:从当前的作用域开始查找,向更高级别的作用域查找,直到最高级别的作用域,查找不到则停止抛出异常(根据RHS以及LHS不同抛出的异常不同)
异常
为什么要知道LHS与RHS查询呢??这对我们调试Java Script代码是有好处的。
例如: function(a){
console.log(a+b);
b=a;
}
foo(b);
我们在foo()中对b进行RHS查询,但是在当前的作用域中查询不到b,根据作用域嵌套规则,我们返回最外一层,发现也没有b。此时我们抛出ReferenceError异常-------即RHS查询不到相关变量则抛出ReferenceError异常。
而LHS则不同,如果在最外层的作用域中查询不到变量的话,它就会自作主张的创建一个变量,此时不会抛出异常
但是在ES5的情况下,LHS表现则不同,在ES5(严格模式)下禁止自动或者隐式创建变量,这就意味着进行LHS查询不能够自动创建变量了!!!-------此时抛出ReferenceError异常。
RHS查询还有一个细节,就是当你查询到变量,但对变量的操作不正确时,也会抛出异常,例如在对一个数组执行大于数组下标的操作时--------抛出TypeError异常。
总结:
-
RHS查询不到变量时抛出ReferenceError异常,查询到变量对变量操作不正确时抛出Type异常
-
LHS在非ES5模式下,查询不到变量时,自动创建变量。在ES5模式下,查询不到变量时,抛出Reference异常