作用域链是 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 时:
- 先在
inner自己的变量对象中查找b→ 未找到。 - 进入下一级
outer的变量对象 → 找到b = 'outer',返回结果。
访问 a 时:
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 变量访问的“高速公路”,理解它的创建与查找,就掌握了变量可见性的本质,也为后续学习闭包、模块化等高级特性打下了坚实基础。