April 20, 2020
作用域(scope)是一套规则,用于确定“如何查找变量”以及“在何处查找变量”。
以“var a = 2”为例,变量的赋值操作会执行两个动作:
作用域是一套在程序运行时控制变量访问的管理机制,规定了变量的可见区域、变量查找规则,嵌套时的检索方法。
编译器为引擎生成了运行时需要的代码,引擎执行这段代码时,会查找变量 a 来判断它是否已经声明过。查找的过程由作用域进行协助,引擎执行查找的方式会影响查找结果,有两种方式:
举例:
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 这几种方式可以生成块作用域。
垃圾回收时,块作用域可以让引擎清楚地知道有没有必要继续保留块作用域里的变量:
function process() {
...
}
// someBigData 执行后可被引擎销毁
{
let someBigData = {...};
process(someBigData);
}
let、const 在 for 循环头部定义,不仅可以将变量绑定到 for 循环的代码块中,还会将变量“重新绑定”到循环的每一次“迭代”中,确保上一个循环结束时的值重新进行赋值。(let声明附属于一个新的作用域,而不是当前函数的作用域):
for (let i=0; i<3; i++) {
console.log(i);
}
console.log(i); // ReferenceError
在 JavaScript 中,作用域和对象类似,可见的标识符都是它的属性,但是作用域无法通过 JavaScript 代码访问,作用域存在于 JavaScript 引擎内部。