人人都会AI编程

3.4 作用域与作用域链

更新时间:2026-07-11

理解了执行上下文和变量对象之后,接下来要解答一个关键问题:当代码中访问一个变量时,引擎是如何找到它的? 答案就藏在作用域与作用域链的机制中。

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 引入):由 {} 包裹的范围(如 iffor 语句块)都能创建块级作用域,前提是使用 letconst 声明变量。var 声明会忽略块级作用域,直接提升到所在的函数作用域(或全局作用域),这是造成很多混淆的根源。
if (true) {
  var a = 1;   // 属于函数作用域(或全局)
  let b = 2;   // 属于当前 if 块级作用域
}
console.log(a); // 1  —— 可访问
console.log(b); // ReferenceError  —— 块外无法访问

3.4.3 作用域链:多层嵌套下的变量查找规则

当一个函数定义在另一个函数内部时,就形成了作用域的嵌套。每一层作用域都有自己的变量对象(或活动对象),这些对象按照嵌套关系串联起来,就构成了作用域链

作用域链的实质:是一个指向变量对象的指针链表(或类似数组的结构)。它规定了代码在执行过程中,对变量进行有序查找的路径。

具体来说,作用域链的建立分为两个阶段:

  1. 函数创建时:引擎将当前函数的 [[Scope]] 内部属性设置为父级作用域的变量对象/活动对象链。也就是说,函数在诞生那一刻,就随身携带了定义它的环境的所有作用域信息。
  2. 函数执行时:一个新的执行上下文被创建,并将当前函数的活动对象(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]] 仍然保持不变,作用域链依然保持着对原始定义环境中变量对象的引用。这就是闭包可以“记住并访问其词法作用域,即使该函数在其词法作用域之外执行”的原因。

关于闭包的详细形成条件、内存影响和应用场景,将在下一节深入展开。在此之前,牢牢掌握“作用域由写下的位置静态决定”、“作用域链是函数定义时父级作用域链与执行时自身活动对象的组合”这两个核心结论,将让你在面对任何复杂嵌套代码时都能从容分析变量访问路径。