JavaScript 作用域与闭包(上):从底层逻辑到实战技巧全解析

117 阅读5分钟

一、作用域:代码里的「隐形地图」

1.1 编译背后的「三步曲」

JavaScript 代码在执行前会经历 词法分析→语法分析→代码生成 三个阶段:

  • 词法分析:把代码字符串拆解成 vara= 等有意义的「词法单元」,就像把句子拆成单词。
  • 语法分析:将词法单元组装成「抽象语法树(AST)」,比如 var a = 2 会被解析成包含变量声明和赋值的树结构。
  • 代码生成:将 AST 转化为可执行代码,同时「提升」变量和函数声明(函数优先于变量提升)。

📝小tips: 任何 JavaScript 代码片段在执行前都要进行编译(通常就在执行前)。因此,JavaScript 编译器首先会对 var a = 2; 这段程序进行编译,然后做好执行它的准备,并且通常会马上执行它。

1.2 作用域是什么?3030d37a78fc3979ec0b144d621a4470.jpeg

为了能更好地理解,我们将这个过程模拟成几个人物之间的对话。 让我们来认识一下三个重要的演员:

• 引擎:从头到尾负责整个 JavaScript 程序的编译及执行过程。

• 编译器:引擎的好朋友🙇‍♀️,负责语法分析及代码生成等脏活累活。

• 作用域:引擎的另一位好朋友🙇‍♀️,负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。

从演员表来看,我们的引擎真是责任重大呀!还好有两位好朋友相助,一起仗剑走天下。

我们来看看 var a = 2; 这段程序是怎么处理的。

20505b7cdc3f70eae2830c415c0fdae3.jpeg

首先编译器会在当前作用域中声明一个变量(如果之前没有声明过),然后在运行时引擎会在作用域中查找该变量,如果能够找到就会对它赋值。

为了进一步理解,我们需要多介绍一点编译器的术语。

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 );
  1. 找到其中所有的 LHS 查询。(这里有 3 处!)
  2. 找到其中所有的 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 的变量环境哈~
c44fe153851ced712d00e7ea136f34dc.jpeg

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异常。

663c7877efce405aa340a995d57f4d63.jpeg

简单总结:
ReferenceError:同作用域判别失败相关。
TypeError:作用域判别成功,但是对结果的操作是非法或不合理的。

小测验答案

function foo(a) {
    var b = a;
    return a + b;
}
var c = foo( 2 );
  1. 找出所有的 LHS 查询(这里有 3 处!)
    c = ..;a = 2(隐式变量分配)b = ..
  2. 找出所有的 RHS 查询(这里有 4 处!)
    foo(2..= a;a .... b

0b0d53714f6e425ecbad6685231e01fd.jpeg