什么是作用域
根据《你不知道的JS》中的描述:
作用域是一组明确定义的规则,它定义如何在某些位置存储变量,以及如何在稍后找到这些变量。
作用域的类型
实际上作用域有两种:
- 动态作用域。
- 静态作用域。
动态作用域中,查找顺序是顺着调用区间向上查找,有点类似洋葱模型。
静态作用于中,查找顺序是顺着方法/属性定义的位置向上查找。
JS 是什么类型的作用域
首先,JS 是静态作用域。我们可以看一下如下例子:
var value = 1;
function test() {
alert(value);
}
function test2() {
var value = 2;
test();
}
test2();
试着分别用两种作用域来分析这段代码
静态作用域:
- 执行
test()
,发现value
在test
中无定义。 - 我们在
test
定义的地方向上查找,最近的value
是全局变量,value = 1
。 - 输出1。
动态作用域:
- 执行
test()
,发现value
在test
中无定义。 - 我们在
test
调用的地方向上查找发现是局部变量value = 2
。 - 输出2
实际上,在 JS 中,这段代码输出的是 1。
为什么会造成这种情况
这个要从编译器讲起。
编译器的不同
在传统的编译型语言中,编译一般会被分为几个步骤:
- 词法分析;
- 解析,生成AST;
- 代码生成;
但是 JS 的引擎会更加复杂,这里不详细研究。我们可以简单的认为 JS 代码在执行前都需要被编译,编译完之后作用域其实就被大致构建出来了,但并不绝对。
理解作用域
编译器在遇到一段 JS 代码的时候,他会将这段代码进行词法解析,分解成一系列的 token
,这些 token
会被解析成 AST语法树,但是到了生成代码阶段,编译器会做如下操作:
var value = 1;
- 编码器遇到
var value
,会先判断var value
是否已经声明,如果已经声明了忽略,否则就会在当前作用域创建一个value
变量。 - 编译器为引擎生成稍后要执行的代码,来处理下一步操作
value = 1
。换句话说引擎才是执行的关键。引擎运行代码会首先在当前作用域进行查找value
变量,如果有,引擎就会使用这个value
,否则的话继续向上查找。
编译器术语
我们在上边的解释中已经知道,value = 1
这个过程会先去查找value
这个变量,如果有我们会给它赋值,用学术一些的话来描述就是:value = 1
是一个 LHS 查询。综上,我们可以理解为 LHS
是引擎为了赋值而进行查找一个变量的动作。
反之,如果查找一个变量不是为了赋值,是为了得到这个变量的值进行一些操作,这个过程就是 RHS
。如下:
var value = 1;
console.log(value);
引擎查找value
是为了打印其值。
举一个文章中的例子:
function foo(a) { // 这个地方很多人会忽略,这个形参其实是一个 LHS ,我们需要查找一个 a 进行赋值 a = 2
console.log( a ); // 这个地方的 a 其实是一个 RHS
}
foo( 2 ); // 这个地方的 foo 其实是一个 RHS ,我们需要获取 foo 并去执行。
以上是比较简单的询问过程,接下来我们增加一些难度。
之前我们说过,在代码执行阶段,引擎在当前作用域查询变量的时候,如果没有找到变量就会向上查找,如下:
function foo(a) {
console.log( a + b );
}
var b = 2;
foo( 2 ); // 4
这段代码仅有一个地方与众不同,就是a + b
,b
进行了一次 RSH
发现 foo
内并没有 b
变量, 这个时候引擎就会跨出当前作用域,在其上级作用域进行查询,发现全局有一个 b
便拿去使用。但是事情总会有例外,如下:
function foo(a) {
console.log( a + b );
}
foo( 2 ); // ReferenceError => b is not defined.
这个上边的片段中, b
在进行 RSH
的时候,发现从头到尾都没有被声明过,这个时候引擎就会报 ReferenceError
。凡事都有例外,当引擎在进行 LSH
查询的时候就是正常的。
function foo(a) {
b = a;
console.log( b );
}
foo( 2 ); // 2
在这里引擎从作用域中无法找到变量 b
,这个时候 全局作用域就会主动创建一个 b
。
总结
编译器解析了代码,并构建了作用域,生成了可执行代码。JS 引擎执行代码的过程中,不断的进行RSH
,LSH
查询,整个过程就是反复的查找作用域中的变量,RSH
是取值来用,因此不会帮你创建变量,会报错。LSH
是找变量来赋值,找不到变量就让作用域创建,严格模式下也会报错。