JS 作用域

avatar
Ctrl+C、V工程师 @豌豆公主

什么是作用域

根据《你不知道的JS》中的描述:

作用域是一组明确定义的规则,它定义如何在某些位置存储变量,以及如何在稍后找到这些变量。

作用域的类型

实际上作用域有两种:

  1. 动态作用域。
  2. 静态作用域。

动态作用域中,查找顺序是顺着调用区间向上查找,有点类似洋葱模型。

静态作用于中,查找顺序是顺着方法/属性定义的位置向上查找

JS 是什么类型的作用域

首先,JS 是静态作用域。我们可以看一下如下例子:

var value = 1;

function test() {
  alert(value);
}

function test2() {
  var value = 2;
  test();
}

test2();

试着分别用两种作用域来分析这段代码

静态作用域

  1. 执行 test(),发现valuetest中无定义。
  2. 我们在test定义的地方向上查找,最近的value是全局变量,value = 1
  3. 输出1。

动态作用域

  1. 执行 test(),发现valuetest中无定义。
  2. 我们在test调用的地方向上查找发现是局部变量value = 2
  3. 输出2

实际上,在 JS 中,这段代码输出的是 1。

为什么会造成这种情况

这个要从编译器讲起。

编译器的不同

在传统的编译型语言中,编译一般会被分为几个步骤:

  1. 词法分析;
  2. 解析,生成AST;
  3. 代码生成;

但是 JS 的引擎会更加复杂,这里不详细研究。我们可以简单的认为 JS 代码在执行前都需要被编译,编译完之后作用域其实就被大致构建出来了,但并不绝对

理解作用域

编译器在遇到一段 JS 代码的时候,他会将这段代码进行词法解析,分解成一系列的 token ,这些 token 会被解析成 AST语法树,但是到了生成代码阶段,编译器会做如下操作:

var value = 1;
  1. 编码器遇到 var value,会先判断var value是否已经声明,如果已经声明了忽略,否则就会在当前作用域创建一个 value 变量。
  2. 编译器为引擎生成稍后要执行的代码,来处理下一步操作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 + bb 进行了一次 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 引擎执行代码的过程中,不断的进行RSHLSH查询,整个过程就是反复的查找作用域中的变量,RSH是取值来用,因此不会帮你创建变量,会报错。LSH是找变量来赋值,找不到变量就让作用域创建,严格模式下也会报错。