作用域链(Scope Chain)

在 JavaScript 中,作用域链(Scope Chain) 是当前执行上下文的作用域与所有外层嵌套作用域的有序集合,核心作用是解析变量 / 函数的访问权限—— 当代码需要访问一个变量时,JS 引擎会沿着作用域链从 “当前作用域” 到 “外层作用域” 依次查找,直到找到变量或到达全局作用域为止。

一、作用域链的核心基础

要理解作用域链,需先明确两个前提:

  1. 作用域的类型

    • 全局作用域:代码最外层,全局有效,浏览器中挂载到 window
    • 函数作用域:函数内部声明的变量仅函数内有效(var 声明);
    • 块级作用域:{} 包裹的区域(let/const 声明);
  2. 作用域的嵌套:JS 支持作用域嵌套(如函数内嵌套函数),内层作用域可访问外层作用域的变量,反之不行;

  3. 执行上下文:每个函数调用 / 全局代码执行时,会创建一个 “执行上下文”,包含当前作用域、作用域链、this 等信息,作用域链是执行上下文的核心属性。

二、作用域链的构成与查找规则

1. 构成(从内到外)

  • 当前执行上下文的词法环境:即当前作用域(如函数内部、块级作用域),存储当前声明的变量 / 函数;
  • 外层嵌套作用域的词法环境:一层一层向外追溯,直到全局作用域;
  • 全局词法环境:作用域链的终点,存储全局变量 / 函数(如 windowconsole)。

2. 查找规则(核心:“就近原则 + 单向查找”)

  • 查找变量时,先在当前作用域找,找到则直接使用;
  • 若未找到,沿作用域链向上一层查找;
  • 直到全局作用域,若仍未找到,非严格模式下会隐式创建全局变量(严格模式下报错 ReferenceError);
  • 作用域链查找是单向的:内层可访问外层,外层无法访问内层。

三、直观示例解析

示例 1:基础嵌套作用域链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 全局作用域(作用域链的最外层)
var globalVar = "全局变量";

function outer() {
// outer 函数作用域(外层嵌套作用域)
var outerVar = "外层变量";

function inner() {
// inner 函数作用域(当前作用域)
var innerVar = "内层变量";
// 访问变量:沿作用域链查找
console.log(innerVar); // 内层作用域找到 → "内层变量"
console.log(outerVar); // 外层作用域找到 → "外层变量"
console.log(globalVar); // 全局作用域找到 → "全局变量"
console.log(notExistVar); // 全局作用域也没找到 → ReferenceError
}

inner(); // 调用 inner,创建 inner 执行上下文,生成作用域链
}

outer();

inner 函数的作用域链(从内到外):inner 作用域outer 作用域全局作用域

示例 2:块级作用域的作用域链(let/const)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const global = "全局";

if (true) {
// 块级作用域 1
let block1 = "块1";

{
// 块级作用域 2(当前作用域)
let block2 = "块2";
console.log(block2); // 块2 → 当前作用域
console.log(block1); // 块1 → 外层块级作用域
console.log(global); // 全局 → 全局作用域
}

console.log(block2); // ReferenceError → 外层无法访问内层块级作用域
}

内层块的作用域链内层块作用域外层块作用域全局作用域

示例 3:作用域链的 “就近覆盖”

1
2
3
4
5
6
7
8
9
var a = 10; // 全局 a

function fn() {
var a = 20; // 函数内 a 覆盖全局 a
console.log(a); // 20 → 优先取当前作用域的变量
}

fn();
console.log(a); // 10 → 全局作用域的 a 不受影响

四、作用域链的本质:词法作用域(静态作用域)

JS 的作用域链基于词法作用域(静态作用域) —— 作用域链的结构在代码编写阶段(词法分析阶段)就确定,而非执行阶段。换句话说:作用域链由代码的 “嵌套结构” 决定,与函数调用位置无关。

示例:词法作用域的关键验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var x = "全局 x";

function outer() {
var x = "outer x";
return function inner() {
console.log(x); // 找的是 outer 的 x,而非调用位置的 x
};
}

// 把 inner 函数赋值给全局变量
var innerFn = outer();

// 在全局作用域调用 innerFn,但作用域链仍指向 outer 作用域
innerFn(); // 输出 "outer x"(而非 "全局 x")

核心原因:inner 函数的作用域链在定义时就绑定了 outer 作用域,无论 inner 在哪里调用,作用域链都不会改变。

五、作用域链与变量提升的关系

变量提升是 “作用域内的声明提前”,而作用域链是 “多个作用域的查找顺序”,二者结合决定了变量的访问规则:

1
2
3
4
5
6
7
8
9
10
console.log(a); // undefined(全局作用域提升,值为 undefined)
var a = 1;

function fn() {
console.log(a); // undefined(fn 作用域提升,覆盖全局 a)
var a = 2;
}

fn();
console.log(a); // 1(全局 a)

解析

  1. 全局执行上下文:作用域链是 全局作用域a 提升后值为 undefined,赋值后为 1;
  2. fn 执行上下文:作用域链是 fn 作用域全局作用域,fn 内的 a 提升后值为 undefined,优先覆盖全局 a,因此 fn 内打印 undefined

六、作用域链的常见坑点

1. 循环中的 var 无块级作用域

1
2
3
4
5
6
7
8
9
// 错误示例:var 无块级作用域,所有 setTimeout 共享同一个 i
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0); // 输出 3,3,3
}

// 正确示例:let 有块级作用域,每次循环创建新的 i,作用域链独立
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0); // 输出 0,1,2
}

2. 内层作用域未声明变量,误修改全局变量

1
2
3
4
5
6
7
8
9
var num = 10;

function changeNum() {
num = 20; // 未声明 num,沿作用域链找到全局 num 并修改
console.log(num); // 20
}

changeNum();
console.log(num); // 20(全局 num 被修改)

规避:内层作用域使用变量前,务必用 let/const 声明,避免污染外层。

七、总结

  1. 作用域链的核心:执行上下文的作用域集合,用于变量查找,遵循 “从内到外、单向查找” 规则;

  2. 本质:基于词法作用域(静态作用域),由代码编写时的嵌套结构决定,与调用位置无关;

  3. 作用:保障变量的访问权限(内层可访问外层,外层不可访问内层),避免变量冲突;

  4. 最佳实践

    • let/const 声明变量,利用块级作用域细化作用域链,减少全局污染;
    • 避免在内层作用域直接修改外层变量,如需修改可通过函数参数 / 返回值传递;
    • 理解词法作用域,避免 “函数调用位置影响变量查找” 的误区。