词法作用域

在 JavaScript 中,词法作用域(Lexical Scope)(也叫静态作用域)是指:变量 / 函数的作用域由代码编写时的嵌套结构(词法位置)决定,而非运行时的调用位置。简单来说:作用域在 “写代码时” 就确定了,不是 “执行代码时” 才确定

这是 JS 作用域的核心规则,也是作用域链、闭包等特性的底层基础。

一、词法作用域 vs 动态作用域(对比理解)

为了更清晰,先对比 “动态作用域”(JS 不支持,但很多语言如 Bash、Perl 支持):

特性 词法作用域(JS) 动态作用域
作用域确定时机 代码编写 / 编译阶段(静态) 代码执行阶段(动态)
查找变量的依据 代码的嵌套结构(定义位置) 函数的调用栈(调用位置)
核心示例 变量找 “定义时的外层作用域” 变量找 “调用时的外层作用域”

直观示例:词法作用域的核心表现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 全局作用域
var x = "全局变量";

function outer() {
// outer 作用域(词法上是 inner 的外层)
var x = "outer 变量";

function inner() {
// inner 作用域:词法上嵌套在 outer 内
console.log(x); // 找的是「定义时」的外层(outer)的 x,而非调用时的环境
}

return inner;
}

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

// 全局作用域调用 innerFn(调用位置在全局)
innerFn(); // 输出:"outer 变量"(而非 "全局变量")

关键解析

  • inner 函数在定义时嵌套在 outer 函数内,因此它的作用域链永久绑定了 outer 作用域;
  • 即使 inner 被拿到全局调用(调用位置变了),它查找 x 时,依然按照 “定义时的嵌套结构” 找 outer 里的 x,而非调用位置的全局 x—— 这就是词法作用域的核心。

如果是动态作用域,innerFn () 在全局调用,会找全局的 x,输出 “全局变量”,但 JS 是词法作用域,所以结果不同。

二、词法作用域的核心规则

1. “定义位置” 决定作用域,与调用无关

无论函数被传递到哪里、在哪里调用,它的作用域链永远基于 “定义时的词法嵌套”:

1
2
3
4
5
6
7
8
9
10
11
function foo() {
var a = 1;
bar(); // 调用 bar
}

function bar() {
// bar 定义在全局,作用域链只有全局,无法访问 foo 的 a
console.log(a); // ReferenceError: a is not defined
}

foo();

解析:bar 函数定义在全局,即使在 foo 内部调用,它的作用域链也不会包含 foo 作用域,因此找不到 a。

2. 嵌套作用域的 “单向访问”

内层作用域可访问外层作用域的变量(沿词法嵌套向上找),外层无法访问内层:

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

function outer() {
var outerVar = "外层";

function inner() {
var innerVar = "内层";
console.log(outerVar); // 内层可访问外层(词法嵌套)
console.log(global); // 内层可访问全局
}

inner();
console.log(innerVar); // 外层无法访问内层 → ReferenceError
}

outer();

3. 变量查找的 “就近原则”

如果多层嵌套作用域有同名变量,优先找 “定义时” 最近的内层作用域:

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

function fn1() {
var a = 2; // fn1 的 a

function fn2() {
var a = 3; // fn2 的 a
console.log(a); // 3(最近的内层)
}

fn2();
}

fn1();

三、词法作用域的常见误区

误区 1:“调用位置” 影响变量查找

1
2
3
4
5
6
7
8
9
10
11
12
var name = "张三";

function sayName() {
console.log(name);
}

function wrap() {
var name = "李四";
sayName(); // 调用位置在 wrap 内,但 sayName 定义在全局
}

wrap(); // 输出:"张三"(而非 "李四")

解析:sayName 定义在全局,作用域链只有全局,因此无论在哪里调用,都找全局的 name。

误区 2:循环中的 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(每个 setTimeout 绑定当前循环的 i)
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0); // 输出 0,1,2
}

解析:let 声明的 i 属于 “每次循环的块级作用域”(词法上独立),因此每个 setTimeout 能捕获到当前循环的 i;而 var 的 i 属于全局,循环结束后 i=3,所有 setTimeout 都找全局的 i。

四、词法作用域与闭包的关系

闭包的本质就是词法作用域的延伸:当内层函数被外层函数返回并保存到外部时,它依然能通过词法作用域访问外层函数的变量(即使外层函数已执行完毕)。

1
2
3
4
5
6
7
8
9
10
11
12
13
function createCounter() {
var count = 0; // 外层作用域的变量

// inner 定义在 createCounter 内,词法作用域包含 createCounter
return function inner() {
count++;
console.log(count);
};
}

var counter = createCounter();
counter(); // 1(inner 仍能访问 createCounter 的 count)
counter(); // 2

解析:createCounter 执行完毕后,其执行上下文被销毁,但 inner 函数的词法作用域依然绑定了 createCounter 的作用域,因此 count 不会被垃圾回收 —— 这就是闭包,核心依赖词法作用域。

五、总结

  1. 核心定义:词法作用域 = 作用域由 “代码编写时的嵌套位置” 决定,而非 “执行时的调用位置”;

  2. 关键表现:函数的变量查找永远按 “定义时的作用域链”,无论在哪里调用;

  3. 底层价值:是作用域链、闭包的基础,让 JS 代码的作用域可预测(静态分析即可确定);

  4. 实践建议

    • let/const 替代 var,利用块级词法作用域细化变量作用范围;
    • 编写嵌套函数时,明确 “定义位置” 决定变量访问范围,避免依赖调用位置的误区;
    • 理解闭包的本质是词法作用域的延伸,而非 “特殊语法”。