什么是作用域链?词法作用域和动态作用域有什么区别?
简化版
作用域链是变量查找时从当前词法环境一路向外层词法环境查找的链。JavaScript 采用词法作用域,函数能访问哪些外部变量由它定义的位置决定,而不是由调用位置决定。
详细版
- 每个执行上下文都有词法环境,用来保存标识符绑定。
- 变量查找从当前环境开始,找不到就沿外部环境引用继续查。
- 函数的外部词法环境在函数创建时确定,因此闭包能记住定义处变量。
- 动态作用域按调用栈查找变量,JavaScript 普通变量不是这种规则。
with、eval会让作用域分析复杂,工程中应尽量避免。
完整版教学
记忆钩子:JavaScript 函数“出生在哪里”,决定它以后去哪里找外部变量。
一、这题真正考什么
记忆钩子:JavaScript 函数“出生在哪里”,决定它以后去哪里找外部变量。
面试官想看的不是你能不能背一个术语,而是能不能解释 JavaScript 为什么会出现这些看似反直觉的结果。答题时要从执行模型、规范规则和工程边界三个角度展开。
二、底层机制怎么理解
词法环境可以理解成一张变量表加一个 outer 引用。函数创建时会保存对外层词法环境的引用,执行函数时新建自己的环境,再把 outer 指向创建时的外部环境。变量查找就是沿着这些 outer 引用逐级查找。
JavaScript 高频题经常围绕“运行时如何决定结果”展开。只要能把创建阶段、调用方式、查找链路、转换规则或内存可达性说清楚,就能把题目从记忆题讲成原理题。
三、带数字或代码的小推演
函数 inner 在 outer 内部定义,即使 inner 被返回到外部再调用,它查找变量时仍会回到 outer 的词法环境。若 outer 中的 x 是 10,外部调用处另有 x=20,inner 仍优先读到 10。
下面把核心规则压成几行,复习时可以按顺序手推一遍:
变量解析:currentEnv -> outerEnv -> outer.outer -> globalEnv
词法作用域 = 定义位置决定可见变量
闭包 = 函数 + 创建时可访问的外部词法环境
四、执行流程怎么走
可以把这道题放到下面的运行流程里看:
解析函数定义位置
|
创建函数对象并保存 [[Environment]]
|
调用函数时创建本地词法环境
|
本地找不到变量
|
沿 [[Environment]] 指向的外层环境继续查找
流程图的价值是把“现象”还原成“步骤”。面试追问通常就是换一段代码让你预测输出,按流程推比靠直觉猜更稳。
五、对比表格
| 维度 | 词法作用域 | 动态作用域 |
|---|---|---|
| 决定因素 | 函数定义位置 | 函数调用位置 |
| JavaScript 普通变量 | 采用 | 不采用 |
| 可预测性 | 静态分析友好 | 依赖调用链 |
| 闭包解释 | 自然成立 | 难以解释 JS 行为 |
对比题要主动说适用场景和边界。JavaScript 的很多机制都不是“好或坏”,而是历史兼容、运行时灵活性和工程可维护性之间的折中。
六、工程实践和排查思路
- 不要把作用域链和原型链混淆,前者查变量,后者查对象属性。
- 闭包保存的是词法环境引用,不是简单复制一份变量值。
- 遇到输出预测题时,不要只报结果,要按创建、查找、调用、转换、执行顺序解释。
- 生产代码要优先选择可读性强、边界清晰的写法,把语言怪异点限制在工具函数或明确封装里。
七、常见误区与追问
- 误区:函数在哪里调用,就去哪里找外部变量。 要补上规范规则、执行阶段和反例,避免用一句经验话术盖过真正原因。
- 误区:作用域链和原型链是一回事。 要补上规范规则、执行阶段和反例,避免用一句经验话术盖过真正原因。
- 误区:闭包一定会导致内存泄漏。 要补上规范规则、执行阶段和反例,避免用一句经验话术盖过真正原因。
- 追问:为什么闭包能访问已经返回的函数局部变量? 先给规则,再用一小段代码或内存/调用示意解释为什么。
- 追问:块级作用域如何影响作用域链? 先给规则,再用一小段代码或内存/调用示意解释为什么。
- 追问:eval 为什么会影响优化? 先给规则,再用一小段代码或内存/调用示意解释为什么。
八、加强记忆
记住“变量查找看作用域链,属性查找看原型链”。JavaScript 的作用域链由定义位置决定,这就是词法作用域和闭包的根。
复习 JavaScript 基础题时,可以固定按“创建阶段、执行阶段、查找链路、调用方式、类型转换、工程边界”六步组织答案。这个框架能覆盖大多数输出题和原理题。