JavaScript 中的作用域

April 20, 2020

一、定义:

作用域(scope)是一套规则,用于确定“如何查找变量”以及“在何处查找变量”。

以“var a = 2”为例,变量的赋值操作会执行两个动作:

  1. 编译器询问作用域中是否已经有该名称的变量存在于同一个作用域的集合中。如果是,编译器会忽略该声明,继续编译;否则在当前作用域集合中重新声明变量a
  2. 编译器为引擎生成运行时需要的代码,处理 a = 2 这个赋值操作。运行时引擎会在作用域(链)中查找该变量,如果能找到就对它赋值,如果找不到就会抛出异常。

作用域是一套在程序运行时控制变量访问的管理机制,规定了变量的可见区域、变量查找规则,嵌套时的检索方法。

二、如何查找变量

编译器为引擎生成了运行时需要的代码,引擎执行这段代码时,会查找变量 a 来判断它是否已经声明过。查找的过程由作用域进行协助,引擎执行查找的方式会影响查找结果,有两种方式:

  • LHS (赋值操作的目标是谁)
  • RHS(谁是赋值操作的源头)

举例:

console.log(a) 对 a 的引用是 RHS,这里没有对 a 进行赋值,需要查找并取到 a 的值,才能传递给console.log。

a = 2 对 a 的引用是 LHS,我们并不关心 a 当前的值是什么,只是为了给 = 2 这个赋值操作找一个目标。

三、在何处查找变量

LHS 和 RHS 查询都会在当前执行作用域中开始,如果当前执行作用域中没找到变量,则向上级作用域继续查找,最后查找到全局作用域(顶层),无论找没找到都会停止查找。

RHS 查找失败会抛出 ReferenceError 异常,LHS 查找失败会隐式创建一个全局变量(非严格模式下),严格模式下也会抛出 ReferenceError 异常。

四、词法作用域

作用域有两种主要的模型:动态作用域和静态作用域,JavaScript 中的作用域是静态作用于(也叫词法作用域),意味着作用域是由书写代码时函数声明的位置决定的。编译的词法分析阶段基本能够知道变量的位置及声明方式,从而能够预测在执行过程中如何对他们进行查找。

JavaScript 中有两种方式“欺骗”词法作用域:eval 和 with。eval 对代码字符串进行运行(演算),并借此修改已经存在的词法作用域。而 with 将一个对象的引用作为作用域来处理。这两种方式都是“运行时”创建新的词法作用域。

JavaScript 引擎在编译时对作用域的查找进行了优化,但上述的 eval 和 with 两种方式无法被优化(引擎认为这样的优化是无效的,因为运行时作用域有改变),因此代码允许会变慢,不建议使用这两种方式。

五、能够生成作用域的结构

函数作用域

函数作用域是最常见的作用域单元,函数内部声明的变量或函数会在所处的作用域中“隐藏”起来。软件设计中有“最小暴露原则”:应该最小限度地暴露必要内容,而将其他内容“隐藏”起来,比如某个模块或 API 的设计。

块作用域

for (var i=0; i<3; i++) {
	console.log(i);
}
console.log(i); // 3

if (true) {
  var b = 1;
  console.log(b);
}
console.log(b); // 1

上面展示的 for、if 用法看似将变量声明在代码块的内部,但并不会生成块作用域。

with、try/catch、let、const 这几种方式可以生成块作用域。

  1. let、const 为其声明的变量隐式地劫持了所在的块作用域,换句话说:用let、const 将变量附加在一个已存在的块作用域上的行为是隐式的。隐式的代码可读性弱,我们可以显式地创建块作用域,使变量的附属变得清晰。未来重构的话,显式创建的块作用域中的代码可以方便地移动。
  2. 垃圾回收时,块作用域可以让引擎清楚地知道有没有必要继续保留块作用域里的变量:

    function process() {
    	...
    }
    // someBigData 执行后可被引擎销毁
    {
    	let someBigData = {...};
    	process(someBigData);
    }
  3. let、const 在 for 循环头部定义,不仅可以将变量绑定到 for 循环的代码块中,还会将变量“重新绑定”到循环的每一次“迭代”中,确保上一个循环结束时的值重新进行赋值。(let声明附属于一个新的作用域,而不是当前函数的作用域):

    for (let i=0; i<3; i++) {
    	console.log(i);
    }
    console.log(i); // ReferenceError

六、作用域与 this

在 JavaScript 中,作用域和对象类似,可见的标识符都是它的属性,但是作用域无法通过 JavaScript 代码访问,作用域存在于 JavaScript 引擎内部。

  1. 作用域是一套规则,而 this 是个对象。
  2. 作用域在词法分析时确定(编译时),而 this 是在运行时进行绑定的
  3. 箭头函数根据函数所在的词法作用域来决定 this (包含的属性和值)