一、作用域:代码里的「隐形地图」
1.1 编译背后的「三步曲」
JavaScript 代码在执行前会经历 词法分析→语法分析→代码生成 三个阶段:
- 词法分析:把代码字符串拆解成
var、a、=等有意义的「词法单元」,就像把句子拆成单词。 - 语法分析:将词法单元组装成「抽象语法树(AST)」,比如
var a = 2会被解析成包含变量声明和赋值的树结构。 - 代码生成:将 AST 转化为可执行代码,同时「提升」变量和函数声明(函数优先于变量提升)。
📝小tips: 任何 JavaScript 代码片段在执行前都要进行编译(通常就在执行前)。因此,JavaScript 编译器首先会对 var a = 2; 这段程序进行编译,然后做好执行它的准备,并且通常会马上执行它。
1.2 作用域是什么?
为了能更好地理解,我们将这个过程模拟成几个人物之间的对话。 让我们来认识一下三个重要的演员:
• 引擎:从头到尾负责整个 JavaScript 程序的编译及执行过程。
• 编译器:引擎的好朋友🙇♀️,负责语法分析及代码生成等脏活累活。
• 作用域:引擎的另一位好朋友🙇♀️,负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。
从演员表来看,我们的引擎真是责任重大呀!还好有两位好朋友相助,一起仗剑走天下。
我们来看看 var a = 2; 这段程序是怎么处理的。
首先编译器会在当前作用域中声明一个变量(如果之前没有声明过),然后在运行时引擎会在作用域中查找该变量,如果能够找到就会对它赋值。
为了进一步理解,我们需要多介绍一点编译器的术语。
1.3 LHS vs RHS:赋值与取值的「左右之争」
- LHS(左查询) :找赋值目标(如
a = 2找a的容器),失败会隐式创建全局变量(非严格模式)。 - RHS(右查询) :找变量值(如
console.log(a)),失败直接抛ReferenceError。
举个🌰:
console.log(a)中对 a 的引用是一个 RHS 引用,因为这里 a 并没有赋予任何值。相应地,需要查找并取得 a 的值,这样才能将值传递给 console.log(..)。相比之下,例如:
a = 2;这里对 a 的引用则是 LHS 引用,因为实际上我们并不关心当前的值是什么,只是想要为 =2 这个赋值操作找到一个目标。
简单总结:
- LHS:赋值操作的目标是谁
- RHS:谁是赋值操作的源头
小测验:
📝🙌把自己当作引擎,并同作用域进行一次“对话”:
function foo(a) {
var b = a;
return a + b;
}
var c = foo( 2 );
- 找到其中所有的 LHS 查询。(这里有 3 处!)
- 找到其中所有的 RHS 查询。(这里有 4 处!)
答案在最后哦~~🥰
1.4 作用域链:变量查找的「寻宝路线」
-
规则:从当前作用域开始找变量,找不到就向上级作用域逐层查找,直到全局作用域(或失败)。
-
案例:
function foo(a) {
console.log( a + b );
}
var b = 2;
foo( 2 ); // 4
让我们把上面这段代码的处理过程想象成一段对话,这段对话可能是下面这样的。
引擎:作用域兄弟,我需要为
foo进行 RHS 引用。你见过这函数不?
作用域:见过!编译器刚声明了它,是个带参数a的函数,给你。
引擎:谢啦!我这就执行foo(2)。
引擎:对了,我要为参数a进行 LHS 引用。
作用域:foo的形参列表里有它,初始值是undefined,拿走。
引擎:把实参2赋值给a。
引擎:执行console.log(a + b)... 先对console来个 RHS 引用。
作用域:这内置对象我熟,全局作用域直接拿走。
引擎:(翻找)找到log方法了!接着对a做 RHS 引用。
作用域:foo作用域里的a是2,给你。
引擎:轮到b了,来个 RHS 引用。
作用域: 我没听过,不知道,你问别人吧。引擎:foo 的上级作用域兄弟,咦?有眼不识泰山,原来你是全局作用域大哥,太好了。你见过 b 吗?我需要对它进行 RHS 引用。
作用域:当然了,给你吧,b是2。
引擎:计算2 + 2得4,传入log方法打印!
作用域:完事了吱一声,我回收foo的变量环境哈~
1.5 异常
为什么区分 LHS 和 RHS 是一件重要的事情?
答:因为在变量还没有声明(在任何作用域中都无法找到该变量)的情况下,这两种查询的行为是不一样的。
考虑如下代码:
function foo(a) {
console.log( a + b );
b = a;
}
foo( 2 );
-
RHS查询:在第一次对b进行RHS查询是无法找到变量的,因为它未被声明,说明此时RHS查询所有的作用域都找不到这个变量,引擎就会抛出
ReferenceError异常。如果 RHS 查询找到了一个变量,但是你尝试对这个变量的值进行不合理的操作,比如试图对一个非函数类型的值进行函数调用,或着引用 null 或 undefined 类型的值中的属性,那么引擎会抛出另外一种类型的异常,叫作
TypeError异常。 -
LHS查询:
非严格模式:当LHS查询在所有作用域都找不到变量,它会创建一个具有该名称的变量,并返回给引擎。
严格模式:禁止自动或隐式地创建全局变量。因此同样抛出ReferenceError异常。
简单总结:
ReferenceError:同作用域判别失败相关。
TypeError:作用域判别成功,但是对结果的操作是非法或不合理的。
小测验答案
function foo(a) {
var b = a;
return a + b;
}
var c = foo( 2 );
- 找出所有的 LHS 查询(这里有 3 处!)
c = ..;、a = 2(隐式变量分配)、b = .. - 找出所有的 RHS 查询(这里有 4 处!)
foo(2..、= a;、a ..、.. b