一般我们将 JavaScript 归类为 “动态” 或 “解释执行”语言,但事实上JavaScript也是一门编译语言。JavaScript 引起不会有大量的(相比其它编译语言)事件用来进行优化,因为与其他语言不同,JavaScript的编译过程不是发生在构建之前的。对于 JavaScript 来说,大部门情况下编译发生在代码执行前的几微妙的时间内。
在传统的 编译语言的流程中,程序中的一段源代码在执行之前会经历三个步骤:
- 分词/词法分析(Tokenizing/Lexing)
- 解析/语法分析 (Parsing)
- 代码生成
比起那些编译过程只有三个步骤的语言的编译器,JavaScript 引擎要复杂得多
理解作用域
以下面的一段代码为例。
var a = 2
-
引擎
: 负责整个JavaScript程序的编译及执行过程 -
编译器
负责语法分析及代码生成等 -
作用域
负责收集维护由所有声明的标识符(变量) 组成的一些列查询。并实施一套非常严格的规格,确定当前执行的代码对这些标识符的访问权限。
但看到 var a = 2 时,引擎会认为这里有两个完全不同的声明,一个由编译器在编译时处理,另一个则由引擎在运行时处理.
编译器会进行如下处理。
- 遇到 var a,会向当前作用域询问是否已有一个该名称的变量存在同一个作用域的集合中。如果有,编译器会忽略该声明,继续进行编译;否则它会要求作用域在当前作用域的集合中声明一个新的变量,并命名为a。
- 编译器会为引擎生成运行所需的代码。引擎运行时会首先询问作用域,在当前的作用域集合中是否存在一个叫作 a 的遍历。如果是,引擎就会使用这个遍历;如果否,引擎会继续查找该变量。
如果引擎最终找到了 a 变量,就会将 2 赋值给它。否则引擎就会抛出一个异常!
LHS 和 RHS
编译器在编译过程的第二步中生成了代码,引擎执行它时,会通过查找变量 a 来判断它是否已经声明过。查找的过程有作用域进行协助,但是引起执行怎样的查找,会影响最终的查找结构
在上面的例子中,引擎会为变量 a 进行 LHS 查询。另外一个查找的类型叫作 RHS。
如名称一样, 这里的 L 和 R 分别代表 left 和 right。指一个 赋值操作的左侧和右侧
- 如果查找的目的是对变量进行赋值,那么就会使用LHS查询;(当变量出现在赋值操作的左侧)
- 如果查找的目的是获取变量的值,就会使用RHS查询;(当变量出现在赋值操作的右侧)
RHS 查询与简单地查找某个变量的值别无二致, 而LHS查询则是试图找到变量的容器本身,从而可以对其赋值
考虑以下代码:
console.log(a)
上面代码中 a 的引用是一个 RHS 引用,因为这里并没有赋予任何值。相应地,需要查找并取得a的值,这样才能将值传递给 console.log(...)
相反的,例如:
a = 10
这里的 a 的引用则是 LHS 引用,要为 = 2 这个赋值找到一个目标
下面是一个小例子:
function foo(a) {
var b = a
return a + b
}
var c = foo(2)
上面分别有几处 LHS 查询,几处 RHS 查询 ?
分别有 3 处 LHS, 4 处 RHS
LHS:
- c = ...
- a = ... 这个是容易忽略的一个 函数的形参,在调用函数时,会进行隐式变量分配.等同于 a=2
- b = ...
RHS:
- foo 函数的查找
- b = a 中 a 的查找
- return 中 a 的查找
- return 中 b 的查找
异常
在变量还没有声明的情况下,这两种查询的行为是不一样的。
function foo(a) {
console.log( a + b )
b = a
}
foo( 2 )
在以上代码中,第一对 b 进行 RHS 查询的时候,是无法找到变量的。
如果在 RHS 查询在所有嵌套的作用域中找不到所需要的变量,引擎就会抛出
ReferenceError
异常。ReferenceError
是非常重要的异常类型当引擎执行 LHS 查询时,如果在所有作用域中找不到目标变量,就会在全局作用域中创建一个具有该名称的变量,并将其返回给引擎,前提是在
非严格模式下
也就是:隐式全局变量
如果 RHS 查询到一个变量,但是你尝试对这个变量的值进行不合理的操作,也会引发错误,比如:
- 试图对一个非函数类型的值进行函数调用,
- 或者引用 null 和 undefined 类型的值中的属性,
那么引擎会抛出另一种类型的异常,叫做 TypeError
ReferenceError
同作用域判别失败相关,而
TypeError
则代表作用域判别成功了,但是对结果的操作是非法或不合理的