《你不知道的javascript》(上)
作用域和闭包
编译原理
JavaScript事实上是一门编译语言。与传统编译语言不同,它不是提前编译的,但是其编译步骤和传统编译语言相似。
1.传统编译流程,代码执行之前经历三个步骤,统称“编译”。
- 分词/词法分析
一段字符组成的字符串被分解成词法单元。空格在具有其实际意义的语言中会被当做词法单元。
- 解析/语法分析
将词法单元数组转换成“抽象语法树”。由元素逐级嵌套所组成,代表程序语法结构的树。
- 代码生成
将“抽象语法树”转换为可执行代码的过程。
2.JavaScript编译
JavaScript引擎没有大量时间优化,JavaScript的编译过程不是发生在构建之前,大部分情况
下编译发生在代码执行之前的极短的时间内。
任何JavaScript代码片段在执行之前都要进行编译。通常会马上执行它。
理解作用域
演员表
-
引擎
从头到尾负责整个JavaScript程序的编译及执行过程。
-
编译器
负责语法分析及代码生成。
-
作用域
收集并维护由所有声明的变量组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些变量的访问权限。
对话
- var a = 2 编译器处理如下
1.遇到var a,编译器询问作用域是否已经有该名称的变量存在于作用域的集合中。有,编译器忽略该声明,继续编译;没有,则会要求作用域在当前作用域的集合中声明一个新的变量,命名为a.
2.接下来编译器会为引擎生成运行时所需要的代码。引擎运行时会首先询问作用域,在当前作用域集合中是否存在一个叫做a的变量。是,引用该变量;否,继续查找该变量。若引擎最终找到了变量a,将2赋值给它。否则引擎会抛出异常。
变量的赋值操作会执行两个动作,首先编译器会在当前的作用域中声明一个变量,如果之前没有声明过,然后在运行时引擎会在作用域查找该变量,如果找到就会对它赋值。如果找不到则会抛出异常。
编译器有话说
RHS 查询与简单地查找某个变量的值别无二致,而 LHS 查询则是试图找到变量的容器本身,从而可以对其赋值。从这个角度说, RHS 并不是真正意义上的“赋
值操作的右侧”, 更准确地说是“非左侧”。
例:console.log(a); 其中对a的引用是一个RHS引用,因为这里并没有对a赋值。只需要查找到a,并取到a对应的值。
LHS 和 RHS 的含义是“赋值操作的左侧或右侧”并不一定意味着就是“=赋值操作符的左侧或右侧”。赋值操作还有其他几种形式,因此在概念上最好将其理解为“赋值操作的目标是谁(LHS)”以及“谁是赋值操作的源头
(RHS)”。
引擎作用域的对话
- 小测验
function foo(a) {
var b = a;
return a + b;
}
var c = foo( 2 );
LHS
c = ..;、 a = 2(隐式变量分配)、 b = ..
RHS
foo(2..、 = a;、 a ..、 .. b
作用域嵌套
作用域是根据名称查找变量的一套规则。
在当前作用域无法找到某个变量,引擎会在外层嵌套的作用域中继续查找,直到找到该变量。
或抵达最外层作用域即全局作用域为止。
异常
如果 RHS 查询在所有嵌套的作用域中遍寻不到所需的变量,引擎就会抛出 ReferenceError异常。值得注意的是, ReferenceError 是非常重要的异常类型。
相较之下,当引擎执行 LHS 查询时,如果在顶层(全局作用域)中也无法找到目标变量,全局作用域中就会创建一个具有该名称的变量,并将其返还给引擎,前提是程序运行在非“严格模式”下。
总结
-
作用域是一套规则,用于确定在何处以及如何查找变量(标识符)。
-
如果查找的目的是对变量进行赋值,那么就会使用 LHS 查询;如果目的是获取变量的值,就会使用 RHS 查询。
-
赋值操作符会导致 LHS 查询。 = 操作符或调用函数时传入参数的操作都会导致关联作用域的赋值操作。
-
JavaScript 引擎首先会在代码执行前对其进行编译
-
LHS 和 RHS 查询都会在当前执行作用域中开始,如果有需要(也就是说它们没有找到所需的标识符),就会向上级作用域继续查找目标标识符,这样每次上升一级作用域(一层楼),最后抵达全局作用域(顶层),无论找到或没找到都将停止。
-
不成功的 RHS 引用会导致抛出 ReferenceError 异常。不成功的 LHS 引用会导致自动隐式地创建一个全局变量(非严格模式下),该变量使用 LHS 引用的目标作为标识符,或者抛出 ReferenceError 异常(严格模式下)。