编译原理
JavaScript事实上是一门编译语言,但与传统的编译语言不同,它不是提前编译的,编译结果也不能在分布式系统中进行移植。
任何JavaScript代码片段在执行前都要进行编译(通常就在执行前)。
在传统编译语言的流程中,程序中的一段源代码在执行之前会经历三个步骤,统称为“”编译“”。
1.分词/词法分析(Tokenizing/Lexing)
将由字符组成的字符串分解成(对编程语言来说)有意义的代码块,这些代码块被称为词法单元。
分词(tokenizing)和词法分析(lexing)之间的区别:
主要差异在于词法单元的识别是通过有状态还是无状态的方式进行的。
如果词法单元生成器在判断a是一个独立的词法单元还是其他词法单元的一部分时,调用的是有状态的解析规则,那么这个过程就被称为词法分析。
2.解析/语法分析(parsing)
将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树。这个树被称为“”抽象语法树“”(Abstract Syntax Tree,AST)。
3.代码生成
将AST转换为可执行代码的过程被称为代码生成。
理解作用域:
引擎:
从头到尾负责整个JavaScript程序的编译及执行过程。
编译器:
引擎的好朋友之一,负责语法分析及代码生成等脏活累活
作用域:
引擎的另一位好朋友,负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。
变量的赋值操作会执行两个动作,首先编译器会在当前作用域中声明一个变量(如果之前没有声明过),然后在运行时引擎会在作用域中查找该变量,如果能够找到就会对它赋值。
引擎的查找 LHS和RHS
当变量出现在赋值操作的左侧时进行LHS查询,出现在右侧时进行RHS查询。
RHS查询与简单地查找某个变量的值别无二致,而LHS查询则是试图找到变量的容器本身,从而可以对其赋值。
RHS并不是真正意义上的“赋值操作符的右侧”,更准确地说是“非左侧”。
赋值操作的目标是谁(LHS)??
谁是赋值操作的源头(RHS)??
作用域嵌套
遍历嵌套作用域链的规则:引擎从当前的执行作用域开始查找变量,如果找不到,就向上一级继续查找。当抵达最外层的全局作用域时,无论找到还是没找到,查找过程都会停止。
异常
如果RHS查询在所有嵌套的作用域中遍寻不到所需的变量,引擎就会抛出referenceError异常。值得注意的是,referenceError是非常重要的异常类型。
当引擎执行LHS查询时,如果在顶层(全局作用域)中也无法找到目标变量,全局作用域中就会创建一个具有该名称的变量,并将其返还给引擎,前提是程序运行在非“严格模式”下。
在严格模式中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异常(严格模式下)。