01作用域与闭包

「变量去哪了?」——这是 JS 面试第一问,也是写代码最容易踩的坑。 本课把作用域链、变量提升、闭包三件事串成一条线,让你彻底搞懂「为什么内层能读外层、外层读不到内层」, 以及闭包到底用来干什么。

约 16 分钟 4 个动手实验 JS 核心概念

一、作用域:变量的「可见范围」

作用域(Scope)规定了「哪里能访问哪个变量」。 JS 有三种作用域:

类型关键字可见范围常见坑
全局作用域 顶层声明(不推荐) 整个页面 / 模块 污染全局,容易命名冲突
函数作用域 function 内 / var 整个函数体 var 在 for 循环里共享同一变量
块级作用域 let / const { } 花括号内 暂时性死区(TDZ)——声明前访问会报错
三种作用域对比:点按钮看 var / let / const 在块里的可见性差异
默认永远用 let / const。 var 只在维护老代码时需要认识。for 循环计数器用 let, 每次迭代都有自己的绑定,闭包拿到的是「本轮的 j」。

二、作用域链:由内向外查找

当你在函数里写一个变量名,JS 会从当前作用域开始,一层层往外找, 找到就用,找到全局还没有就抛 ReferenceError。这条链子叫 作用域链(Scope Chain)。

全局作用域 app · color · run()
↑ 找不到就继续往外
函数 outer size · inner()
↑ 找不到就继续往外
函数 inner name · 在这里用 color / size
作用域链查找:在最内层函数里依次访问三层变量,看查找顺序
作用域链在函数定义时就确定了(词法作用域), 不是调用时。所以 inner 永远从「它写在哪里」往外找,而不是从「谁调用它」往外找。

三、闭包:函数「记住」自己的出生地

闭包(Closure)= 函数 + 它出生时所在的作用域。 当一个内部函数被外部「带走」(返回 / 赋值给变量 / 当回调), 它仍然能读到外层函数的变量——外层函数已经执行完了,变量却还活着。

定义

内部函数引用了外部函数的变量,且内部函数被外部访问到。

本质

不是「函数嵌套」那么简单——关键是外部函数执行完后变量仍被持有。

用途

数据私有、缓存、函数工厂、防抖节流、React hooks 底层。

经典计数器:makeCounter 被调用两次,各拿到独立的 count——这就是闭包
动手练习: 把 makeCounter 改成只有一个 inc 方法, 并用 let count = 0 放在全局—— 再点按钮,你会发现 A 和 B 会互相干扰。这就是「没有闭包私有」的后果。

四、闭包的三个实战场景

01

数据私有 / 模块

对外只暴露方法,内部变量外界碰不到——ES6 class 私有字段出现前的标准做法。

02

函数工厂

一次配置,多次使用。例如 makeAdder(10) 返回「加 10 的函数」。

03

防抖 / 节流

用闭包保存 timer,控制高频事件(scroll / resize / input)触发频率。

函数工厂 + 防抖:makeAdder 生成定制函数;输入框防抖 500ms 才输出

五、闭包的坑:内存与循环

坑现象解法
内存泄漏 闭包长期持有大对象(DOM、大数组),页面不释放 用完把引用置 null;避免无意义的外层大对象捕获
for + var 循环里的回调全部拿到「最后一个 i」 改用 let i,或 IIFE 包一层
误以为闭包很慢 过度优化,不敢用闭包 闭包本身开销很小,真正的问题是「持有不该持有的引用」
for + var 的经典 bug:三个按钮都显示 3;改成 let 后各显示自己的下标

六、词法作用域 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() 方法。