01作用域与闭包
「变量去哪了?」——这是 JS 面试第一问,也是写代码最容易踩的坑。 本课把作用域链、变量提升、闭包三件事串成一条线,让你彻底搞懂「为什么内层能读外层、外层读不到内层」, 以及闭包到底用来干什么。
一、作用域:变量的「可见范围」
作用域(Scope)规定了「哪里能访问哪个变量」。 JS 有三种作用域:
| 类型 | 关键字 | 可见范围 | 常见坑 |
|---|---|---|---|
| 全局作用域 | 顶层声明(不推荐) | 整个页面 / 模块 | 污染全局,容易命名冲突 |
| 函数作用域 | function 内 / var | 整个函数体 | var 在 for 循环里共享同一变量 |
| 块级作用域 | let / const | { } 花括号内 |
暂时性死区(TDZ)——声明前访问会报错 |
点击按钮看结果
let / const。
var 只在维护老代码时需要认识。for 循环计数器用 let,
每次迭代都有自己的绑定,闭包拿到的是「本轮的 j」。二、作用域链:由内向外查找
当你在函数里写一个变量名,JS 会从当前作用域开始,一层层往外找,
找到就用,找到全局还没有就抛 ReferenceError。这条链子叫
作用域链(Scope Chain)。
点击按钮看查找过程
三、闭包:函数「记住」自己的出生地
闭包(Closure)= 函数 + 它出生时所在的作用域。 当一个内部函数被外部「带走」(返回 / 赋值给变量 / 当回调), 它仍然能读到外层函数的变量——外层函数已经执行完了,变量却还活着。
内部函数引用了外部函数的变量,且内部函数被外部访问到。
不是「函数嵌套」那么简单——关键是外部函数执行完后变量仍被持有。
数据私有、缓存、函数工厂、防抖节流、React hooks 底层。
点按钮看两个计数器互不干扰
makeCounter 改成只有一个 inc 方法,
并用 let count = 0 放在全局——
再点按钮,你会发现 A 和 B 会互相干扰。这就是「没有闭包私有」的后果。四、闭包的三个实战场景
数据私有 / 模块
对外只暴露方法,内部变量外界碰不到——ES6 class 私有字段出现前的标准做法。
函数工厂
一次配置,多次使用。例如 makeAdder(10) 返回「加 10 的函数」。
防抖 / 节流
用闭包保存 timer,控制高频事件(scroll / resize / input)触发频率。
函数工厂:
防抖输入(停 500ms 才算一次):
操作上方按钮 / 输入框
五、闭包的坑:内存与循环
| 坑 | 现象 | 解法 |
|---|---|---|
| 内存泄漏 | 闭包长期持有大对象(DOM、大数组),页面不释放 | 用完把引用置 null;避免无意义的外层大对象捕获 |
| for + var | 循环里的回调全部拿到「最后一个 i」 | 改用 let i,或 IIFE 包一层 |
| 误以为闭包很慢 | 过度优化,不敢用闭包 | 闭包本身开销很小,真正的问题是「持有不该持有的引用」 |
点按钮对比两种写法
六、词法作用域 vs 动态作用域
JS 是词法作用域(静态):作用域链在写代码时就定了。
有些语言(如 bash)是动态作用域——看「谁调用我」。理解这一点,才能明白
this(下节课)为什么和作用域是两套规则。
七、和后续知识的连接
- this:作用域链看「定义在哪」,
this看「怎么调用」——两套独立规则,下节课专讲。 - 模块化:ES Module 每个文件是一个独立作用域,天然避免全局污染(第 11 章)。
- 异步:回调 / Promise / async 都大量依赖闭包保存状态。
- 框架:React
useState的 setter 就是闭包;Vue 的响应式也靠闭包持有依赖。
八、本章小结
- 三种作用域:全局 / 函数(
var)/ 块级(let/const)。 默认 let/const。 - 作用域链由内向外查找,定义时锁定(词法作用域)。
- 闭包 = 函数 + 出生作用域:内部函数被外部持有,仍能读外层变量。
- 闭包三大用途:数据私有、函数工厂、防抖节流。
- 经典坑:
for (var i…)闭包共享 i → 改let。 - 作用域管「变量可见性」,
this管「谁调用」——分开记。
makeGreeter(greeting),返回「拼接名字」的函数;
② 写一个节流版的滚动监听(提示:闭包保存上次执行时间戳);
③ 把本课计数器改成支持 reset() 方法。