几乎所有编程语言最基本的功能之一,就是能够储存变量当中的值,并且能在之后对这个 值进行访问或修改。事实上,正是这种储存和访问变量的值的能力将状态带给了程序。
需要一套设计良好的规则来存储变量,以便能够方便的找到这些变量,这套规则称为作用域。
编译原理
在传统编译语言的流程中,程序中的一段源代码在执行之前会经历三个步骤,统称为“编 译”。
分词/词法分析
解析/语法分析
代码生成
比起那些编译过程只有三个步骤的语言的编译器,JavaScript 引擎要复杂得多。
大部分情况下编译发生在代码执行前的几微秒(甚至更短!)的时 间内。在我们所要讨论的作用域背后,JavaScript 引擎用尽了各种办法(比如 JIT,可以延 迟编译甚至实施重编译)来保证性能最佳。
变量的赋值
变量的赋值操作会执行两个动作,首先编译器会在当前作用域中声明一个变量(如 果之前没有声明过),然后在运行时引擎会在作用域中查找该变量,如果能够找到就会对 它赋值。
引擎、编译器、作用域之间的关系
- 引擎负责整个JAvaScript程序的编译以及执行过程
- 编译器,复杂语法分析以及代码生成等脏活累活
- 作用域,复杂手机并维护由所有声明的标识符组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。
编译器遇到一个变量是如何解析的
- 遇到var a,编译器询问作用域是否已经有了一个该名称的变量存在于同一个作用域的集合中,如果是,编译器会忽略该声明,继续进行编译。否则,他会要求作用域在当前作用域中声明一个新的变量,并命名为a
- 接下编译器会为引擎生成运行时所需的代码,如果当前的作用域集合中存在一个变量a,如果是,则会使用这个变量,如果不是,则会继续查找该变量。
LHS、RHS
如果查找的目的是对变量进行赋值,就会使用 LHS 查询;如果目的是获取变量的值,就会用 RHS 查询。
一个简单的例子:
1 | console.log(a) //RHS,只需要查询并获取a的值 |
在函数调用中:
1 | function foo(a) { |
- 最后一行 foo(..) 函数的调用需要对 foo 进行 RHS 引用,意味着“去找到 foo 的值,并把 它给我”。
- 为了给参数a(隐式的)赋值,需要进进行一次LHS查询
- 打印输出语句中,会对 console 对象进行 RHS 查询,并且检查 得到的值中是否有一个叫作 log 的方法。
作用域嵌套
遍历嵌套作用域的规则很简单:引擎从当前的执行作用域开始查找变量,如果找不到,就向上一级继续继续查找,当抵达最外层的全局作用域是,无论找到还是没找到,查找过程都会停止。
异常
如果 RHS 查询在所有嵌套的作用域中遍寻不到所需的变量,引擎就会抛出 ReferenceError 异常。值得注意的是,ReferenceError 是非常重要的异常类型。
相较之下,当引擎执行 LHS 查询时,如果在顶层(全局作用域)中也无法找到目标变量,
全局作用域中就会创建一个具有该名称的变量,并将其返还给引擎,前提是程序运行在非 “严格模式”下。
小结
- 作用域是一套规则,用于确定在何处以及如何查找变量(标识符)。
- 如果查找的目的是对变量进行赋值,就会使用 LHS 查询;如果目的是获取变量的值,就会用 RHS 查询。
赋值操作会导致 LHS 查询。 = 操作符或调用函数时传入参数的操作都会导致关联作用域的赋值操作, 即都会导致 LHS 查询。
- JavaScript 引擎首先会在代码执行前对其进行编译,在这个过程中,像var a = 2 这样的声明会被分解成两个独立的步骤:
首先,var a在其作用域中声明新变量。这会在最开始的阶段,也就是代码执行前进行。
接下来,a = 2会查询(LHS查询)变量 a 并对其进行赋值。
- LHS 和 RHS 查询都会在当前执行作用域中开始,如果有需要(也就是说它们没有找到所需的标识符),就会向上级作用域继续查找目标标识符,这样每次上升一级作用域,最后到达全局作用域,无论找到或没找到都将停止。
不成功的 RHS 引用会导致抛出 ReferenceError 异常。不成功的 LHS 引用会导致自动隐式地创建一个全局变量(非严格模式下),该变量使用 LHS 引用的目标作为标识符,或者抛出 ReferenceError 异常(严格模式下)。