作用域是什么?
变量是 JavaScript 程序能“动起来”的基础。
JavaScript中的变量是如何储存的,以及后续调用变量时该如何找到变量呢?
我们需要制定一套规则来规划变量。这套规则就被称为作用域。
编译原理
JavaScript 的编译方式并非提前编译,编译结果也不能在分布式系统中进行移植,它的编译过程比传统的编译过程(词法分析、语法分析、代码生成)要复杂得多。
不同于其他的编译,JavaScript 的大部分编译发生在代码执行前的一瞬间,流程大概就是,当你要用到了 var a = 2 这段代码片段了,JavaScript 编译器会对这个代码片段进行编译,然后做好执行它的准备,并且通常马上就会执行。
理解作用域
请允许我介绍以下三位在编译时和运行时都举足轻重的角色。可以说经由它们仨的编排,才让机器得以理解你所有JavaScript代码的意义。它们分别是:
引擎:从头到尾负责整个JavaScript程序的编译及执行过程。
编译器:负责语法分析及代码生成等。
作用域:负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。
下面我们将以 var a = 2; 代码块为例,解释这三个角色的分工。
遇到声明a操作(var a)时,编译器会先询问作用域是否有 a 变量存在于作用域的集合中,如果有,编译器会忽略该声明;否则它会让作用域在当前作用域的集合中声明一个新的变量,并命名为 a。
接下来编译器会为引擎生成运行时所需的代码,这些代码用来处理后续的赋值操作(a = 2)。引擎运行时会首先询问作用域,在当前的作用域集合中是否存在变量 a。如果是,引擎就会使用这个变量并将 2 赋值给它;如果否,引擎将抛出一个异常。
接下来介绍两种变量查询方式:
LHS:查询赋值操作的目标是谁?
RHS:查询谁是赋值操作的源头?
我们分析一下以下代码中执行过多少次 LHS 和 RHS。
function foo (a) {
console.log (a);
}
foo (2);
foo(2)需要对 foo 进行 RHS 查询,找到 foo 的值,并执行 foo。
隐式的 a = 2 操作,需要对 a 进行一次 LHS 查询,将 2 赋值给目标 a。
console.log(a) 操作中,首先需要对 a 进行一次 RHS 查询,找到 a 的值,并传递给 log;
其次是对 console 对象进行 RHS 查询,并检查该对象是否有 log 方法;
最后,在概念上可以理解为在 LHS 和 RHS 之间通过对值 2 进行交互,来将其传递进 log,并执行 log 方法。
检测一下到目前的理解程度,试图找出以下代码执行过多少次 LHS 和 RHS 吧。
function foo(a) {
var b = a;
return a + b;
}
var c = foo(2);
作用域嵌套
当一个块或函数嵌套在另一个块或函数中时,就发生了作用域的嵌套。因此,在当前作用域中无法找到某个变量时,引擎就会在完成给钱套的作用域中继续查找,直到找到该变量,或抵达最外层的作用域(也就是全局作用域)为止。
异常
为什么区分 LHS 和 RHS 是一件重要的事情?
当 RHS 查询在所有嵌套的作用域中遍寻不到所需的变量,引擎就会抛出 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异常(严格模式下)。