人人都会AI编程

作用域链的创建与查找机制

更新时间:2026-07-11

作用域链是 JavaScript 实现变量按作用域访问的核心机制。它回答了“当前代码能访问哪些变量”这个基本问题,其创建和查找规则深刻影响着闭包、变量提升、函数嵌套等几乎所有的代码行为。

作用域链的本质

作用域链本质上是一个指向变量对象(Variable Object)的指针列表。每一个执行上下文都有一个与之关联的作用域链。当需要查找某个变量时,引擎会沿着这条链从当前作用域开始逐级向上查找,直到找到该变量或到达全局作用域(链的末端)。

作用域链的创建

作用域链的创建遵循词法作用域(静态作用域) 的规则,即函数的作用域在函数定义时就确定了,与函数在哪里被调用无关。这意味着只看代码写在哪里,就能推断出作用域链的结构。

创建过程分为两个阶段:

1. 函数创建时:保存父级作用域链

每个函数在定义时,都会在内部属性 [[Scopes]] 中保存其父级作用域链(也就是当前执行上下文的作用域链)。这个保存操作是静态的,发生在词法解析阶段,不会等到调用时才去计算。

let a = 'global';

function outer() {
    let b = 'outer';
    
    function inner() {
        let c = 'inner';
        console.log(a, b, c); // inner 能访问 a,b,c
    }
    
    inner();
}

outer();

在这个例子中:

  • 全局作用域保存了变量对象 { a, outer }
  • outer 函数定义时,其 [[Scopes]] 保存了 [全局作用域的变量对象]
  • inner 函数定义时,其 [[Scopes]] 保存了 [outer 函数作用域的变量对象, 全局作用域的变量对象]

2. 函数调用时:构建完整作用域链

当函数被调用时,会创建该函数的执行上下文。此时会取出 [[Scopes]] 中保存的父级作用域链,再在链的最前端添加当前函数自己的变量对象(活动对象),形成完整的、适用于本次调用的作用域链。

outer() 调用为例:

  • 全局执行上下文的作用域链: [全局变量对象]
  • 调用 outer 时,构建作用域链: [outer 的变量对象] + outer.[[Scopes]][outer 的变量对象, 全局变量对象]
  • 调用 inner 时,构建作用域链: [inner 的变量对象, outer 的变量对象, 全局变量对象]

这样,作用域链就串联起了从内到外的所有变量对象。

作用域链的查找机制

查找机制很简单:从作用域链的最前端(当前作用域)开始,依次向后查找变量对象,一旦找到即停止并返回;若查完整个链仍未找到,则抛出 ReferenceError

inner 内部访问变量 b 时:

  1. 先在 inner 自己的变量对象中查找 b → 未找到。
  2. 进入下一级 outer 的变量对象 → 找到 b = 'outer',返回结果。

访问 a 时:

  1. inner 没有 → outer 没有 → 全局有,返回 'global'

就近原则导致了内层变量会“遮蔽”外层同名变量,这种遮蔽正是作用域链按序查找的直接结果。

作用域链与动态查找的对比

再次强调,JavaScript 采用的是词法(静态)作用域,作用域链在定义时即确定。即使把 inner 函数拿到其他地方执行(如通过 return 暴露给外部),它访问的仍然是定义它时的那个 outer 作用域,而非调用时的上下文。这正是闭包得以工作的根本原因——函数保留了对定义时作用域链的引用,无论何时何地执行,查找都会沿着这条“固定”的链进行。

function createAdder(x) {
    return function(y) {
        return x + y; // x 来自定义时的 createAdder 作用域
    };
}

const add5 = createAdder(5);
console.log(add5(3)); // 8,x 始终是 5

这里返回的匿名函数在 createAdder(5) 调用时被创建,它的 [[Scopes]] 包含了 createAdder 调用时的变量对象(其中 x = 5)。无论 add5 在哪里被调用,它都会沿着这条作用域链找到 x = 5

理解作用域链的实用价值

  • 调试:在浏览器 DevTools 中设置断点,观察 Scope 面板能看到当前作用域链的结构,可以直观确认每个变量来自哪个作用域。
  • 性能:作用域链越长,变量查找需要遍历的层级就越多。频繁访问全局变量比访问局部变量慢一点点(现代引擎对此有优化,但理解这条原则有助于写出更清晰的代码)。
  • 闭包:不当使用闭包可能导致内存泄漏,因为作用域链上的变量对象在函数不再需要时可能仍被保留。理解作用域链的保留机制有助于诊断这类问题(详见 3.5 节)。

作用域链是 JavaScript 变量访问的“高速公路”,理解它的创建与查找,就掌握了变量可见性的本质,也为后续学习闭包、模块化等高级特性打下了坚实基础。