词法作用域
在 JavaScript 中,词法作用域(Lexical Scope)(也叫静态作用域)是指:变量 / 函数的作用域由代码编写时的嵌套结构(词法位置)决定,而非运行时的调用位置。简单来说:作用域在 “写代码时” 就确定了,不是 “执行代码时” 才确定。
这是 JS 作用域的核心规则,也是作用域链、闭包等特性的底层基础。
一、词法作用域 vs 动态作用域(对比理解)
为了更清晰,先对比 “动态作用域”(JS 不支持,但很多语言如 Bash、Perl 支持):
| 特性 | 词法作用域(JS) | 动态作用域 |
|---|---|---|
| 作用域确定时机 | 代码编写 / 编译阶段(静态) | 代码执行阶段(动态) |
| 查找变量的依据 | 代码的嵌套结构(定义位置) | 函数的调用栈(调用位置) |
| 核心示例 | 变量找 “定义时的外层作用域” | 变量找 “调用时的外层作用域” |
直观示例:词法作用域的核心表现
1 | // 全局作用域 |
关键解析:
- inner 函数在定义时嵌套在 outer 函数内,因此它的作用域链永久绑定了 outer 作用域;
- 即使 inner 被拿到全局调用(调用位置变了),它查找 x 时,依然按照 “定义时的嵌套结构” 找 outer 里的 x,而非调用位置的全局 x—— 这就是词法作用域的核心。
如果是动态作用域,innerFn () 在全局调用,会找全局的 x,输出 “全局变量”,但 JS 是词法作用域,所以结果不同。
二、词法作用域的核心规则
1. “定义位置” 决定作用域,与调用无关
无论函数被传递到哪里、在哪里调用,它的作用域链永远基于 “定义时的词法嵌套”:
1 | function foo() { |
解析:bar 函数定义在全局,即使在 foo 内部调用,它的作用域链也不会包含 foo 作用域,因此找不到 a。
2. 嵌套作用域的 “单向访问”
内层作用域可访问外层作用域的变量(沿词法嵌套向上找),外层无法访问内层:
1 | var global = "全局"; |
3. 变量查找的 “就近原则”
如果多层嵌套作用域有同名变量,优先找 “定义时” 最近的内层作用域:
1 | var a = 1; // 全局 a |
三、词法作用域的常见误区
误区 1:“调用位置” 影响变量查找
1 | var name = "张三"; |
解析:sayName 定义在全局,作用域链只有全局,因此无论在哪里调用,都找全局的 name。
误区 2:循环中的 var 无块级词法作用域
1 | // 错误:var 无块级作用域,所有 setTimeout 共享同一个 i(词法上都属于全局) |
解析:let 声明的 i 属于 “每次循环的块级作用域”(词法上独立),因此每个 setTimeout 能捕获到当前循环的 i;而 var 的 i 属于全局,循环结束后 i=3,所有 setTimeout 都找全局的 i。
四、词法作用域与闭包的关系
闭包的本质就是词法作用域的延伸:当内层函数被外层函数返回并保存到外部时,它依然能通过词法作用域访问外层函数的变量(即使外层函数已执行完毕)。
1 | function createCounter() { |
解析:createCounter 执行完毕后,其执行上下文被销毁,但 inner 函数的词法作用域依然绑定了 createCounter 的作用域,因此 count 不会被垃圾回收 —— 这就是闭包,核心依赖词法作用域。
五、总结
核心定义:词法作用域 = 作用域由 “代码编写时的嵌套位置” 决定,而非 “执行时的调用位置”;
关键表现:函数的变量查找永远按 “定义时的作用域链”,无论在哪里调用;
底层价值:是作用域链、闭包的基础,让 JS 代码的作用域可预测(静态分析即可确定);
实践建议
:
- 用
let/const替代var,利用块级词法作用域细化变量作用范围; - 编写嵌套函数时,明确 “定义位置” 决定变量访问范围,避免依赖调用位置的误区;
- 理解闭包的本质是词法作用域的延伸,而非 “特殊语法”。
- 用



