理解了执行上下文和变量对象之后,接下来要解答一个关键问题:当代码中访问一个变量时,引擎是如何找到它的? 答案就藏在作用域与作用域链的机制中。
3.4.1 词法作用域:写在哪里,就定在哪里
JavaScript 采用词法作用域(也称静态作用域),也就是说,一个函数的作用域在函数定义时就已经确定了,而不是在执行时才动态决定。这与某些语言(如 Bash、Perl 中的部分特性)使用的动态作用域完全不同。
最直观的理解方式是:看代码的书写位置。函数写在全局,其外层作用域就是全局;函数嵌套在另一个函数内部,其外层作用域就是包裹它的那个函数的作用域。无论这个函数后来被传递到哪里调用,它能访问的变量范围都只由它的定义位置决定。
const outer = 'global';
function foo() {
console.log(outer);
}
function bar() {
const outer = 'local';
foo(); // 输出什么?
}
bar(); // "global" —— 因为 foo 的词法作用域是全局,不受 bar 内部变量影响
这种确定性是分析代码行为的基石,也使得闭包、模块等高级特性拥有可预测的表现。
3.4.2 作用域的分类
JavaScript 中主要存在三种作用域类型:
- 全局作用域:在所有函数和代码块之外定义的变量,整个程序任意位置都可访问(除非被同名局部变量遮蔽)。在浏览器中,全局作用域即是
window,在 Node.js 中是global。 - 函数作用域:在函数内部定义的变量(使用
var声明的变量也属于函数作用域),只能在函数内部访问。外部代码无法直接获取函数内的局部变量,这构成了一个天然的信息隐藏边界。 - 块级作用域(ES6 引入):由
{}包裹的范围(如if、for语句块)都能创建块级作用域,前提是使用let或const声明变量。var声明会忽略块级作用域,直接提升到所在的函数作用域(或全局作用域),这是造成很多混淆的根源。
if (true) {
var a = 1; // 属于函数作用域(或全局)
let b = 2; // 属于当前 if 块级作用域
}
console.log(a); // 1 —— 可访问
console.log(b); // ReferenceError —— 块外无法访问
3.4.3 作用域链:多层嵌套下的变量查找规则
当一个函数定义在另一个函数内部时,就形成了作用域的嵌套。每一层作用域都有自己的变量对象(或活动对象),这些对象按照嵌套关系串联起来,就构成了作用域链。
作用域链的实质:是一个指向变量对象的指针链表(或类似数组的结构)。它规定了代码在执行过程中,对变量进行有序查找的路径。
具体来说,作用域链的建立分为两个阶段:
- 函数创建时:引擎将当前函数的
[[Scope]]内部属性设置为父级作用域的变量对象/活动对象链。也就是说,函数在诞生那一刻,就随身携带了定义它的环境的所有作用域信息。 - 函数执行时:一个新的执行上下文被创建,并将当前函数的活动对象(AO,即局部变量、参数、内部函数声明等)压入作用域链的最前端。此时作用域链的结构是:
[当前函数的活动对象] → [父级作用域的活动对象] → ... → [全局作用域的变量对象]
当函数执行过程中访问一个变量名时,引擎会沿着这条作用域链,从最前端开始向后查找,直到找到第一个匹配的标识符,或者抵达链尾(全局作用域)仍未找到,此时会抛出 ReferenceError(非严格模式下,如果对未声明变量赋值,可能会意外创建全局变量,这是应该严格避免的)。
let globalVar = 'global';
function outer() {
let outerVar = 'outer';
function inner() {
let innerVar = 'inner';
console.log(innerVar); // 在当前活动对象找到
console.log(outerVar); // 在 outer 的活动对象找到
console.log(globalVar); // 在全局变量对象找到
}
inner();
}
outer();
在上述代码中,inner 函数的作用域链为:[inner AO] → [outer AO] → [global VO],变量的查找正是沿着这条路径进行,由内而外,由近及远。
3.4.4 与闭包的内在联系
作用域链是闭包实现的物理基础。当内部函数被返回到外部环境,或者作为回调传递出去之后,该函数的 [[Scope]] 仍然保持不变,作用域链依然保持着对原始定义环境中变量对象的引用。这就是闭包可以“记住并访问其词法作用域,即使该函数在其词法作用域之外执行”的原因。
关于闭包的详细形成条件、内存影响和应用场景,将在下一节深入展开。在此之前,牢牢掌握“作用域由写下的位置静态决定”、“作用域链是函数定义时父级作用域链与执行时自身活动对象的组合”这两个核心结论,将让你在面对任何复杂嵌套代码时都能从容分析变量访问路径。