B01 作用域与闭包
B01 作用域与闭包
摘要:作用域决定标识符在哪里可见,闭包是函数连同其词法环境的组合。变量查找沿作用域链由内向外,函数在哪定义而非在哪调用决定它能看到什么。闭包让私有状态与工厂函数成为可能,代价是延长变量生命周期,循环中误捕获与内存滞留由此产生。本文给出三类作用域、闭包的三处实用写法与两条泄漏判定方法。
关键词:作用域,作用域链,闭包,词法环境,暂时性死区
所属:frontend/js / B-进阶 | 上一篇:A09-实战待办清单 | 下一篇:B02-this与原型链
1 引言
同一行 console.log(count) 在函数内能取到值,在函数外报 count is not defined;循环里注册的十个回调打印出同一个数字;本应销毁的大数组占据内存不释放。三者的共同原因是作用域与闭包。
本文回答三个问题:变量在何处可见、闭包捕获了什么、什么时候闭包会造成滞留。范围见表 1-1。
| 项 | 本文覆盖 | 本文不覆盖 |
|---|---|---|
| 作用域 | 全局、函数、块级、模块 | 模块加载顺序(B05) |
| 闭包 | 捕获机制、工厂、私有状态、柯里化 | 内存回收细节(C03) |
| 声明 | let/const 的暂时性死区、提升 |
属性描述符(C02) |
2 预备知识
| 项 | 要求 | 核对方式 |
|---|---|---|
| 环境 | Node.js 20+ 或浏览器控制台 | 能执行多行脚本 |
| 前置 | A05 | 能写函数与默认参数 |
| 文件 | js-practice/closures.js |
编码 UTF-8 |
3 原理与实现
3.1 词法作用域与作用域链
函数在定义处确定查找链,与调用位置无关,如图 3-1 所示。
flowchart LR
A["全局作用域<br/>global"] --> B["模块作用域<br/>module"]
B --> C["函数作用域 outer"]
C --> D["块级作用域 { let }"]
D --> E["函数作用域 inner"]
E -->|"查找 total"| D
D -->|"未命中"| C
C -->|"未命中"| B
B -->|"未命中"| A
A -->|"仍未命中"| F["ReferenceError"]
图 3-1 标识符按由内向外顺序逐层查找
图上可见:查找是单向的,内层能读外层,外层读不到内层;某一层一旦命中即停止,不会继续向上。
3.2 闭包的四种用法与两处陷阱
// js-practice/closures.js;运行环境:Node.js 20+
// 1 私有状态:外部只能通过方法读写
function createCounter(initial = 0) {
let count = initial;
return {
inc: () => ++count,
dec: () => --count,
get value() {
return count;
},
};
}
const counter = createCounter(10);
counter.inc();
console.log(counter.value); // 11
console.log('count' in counter); // false,无法直接访问
// 2 工厂:把配置固化进闭包,避免重复传参
function createFormatter(prefix) {
return (text) => `${prefix}${text}`;
}
const warn = createFormatter('[warn] ');
console.log(warn('磁盘空间不足')); // [warn] 磁盘空间不足
// 3 一次性初始化:立即执行函数限制作用域
const config = (() => {
const base = 'https://api.example.com';
return { base, timeout: 5000 };
})();
// 4 循环中捕获:let 每轮新建绑定
const fns = [];
for (let i = 0; i < 3; i += 1) {
fns.push(() => i);
}
console.log(fns.map((f) => f())); // [0, 1, 2]
const bad = [];
for (var j = 0; j < 3; j += 1) {
bad.push(() => j);
}
console.log(bad.map((f) => f())); // [3, 3, 3]
// 5 暂时性死区:声明前访问直接报错
try {
console.log(early);
let early = 1;
} catch (err) {
console.log(err.name); // ReferenceError
}
// 6 柯里化:把多参数函数拆成逐参数调用,配置固化在闭包里
const withBase = (base) => (value) => base + value;
const addTax = withBase(0.06);
const addFee = withBase(10);
console.log(addTax(100), addFee(100)); // 106 110
// 函数组合:把多个单参数函数串成一条流水线
const pipe = (...fns) => (value) => fns.reduce((acc, fn) => fn(acc), value);
const normalize = pipe(
(s) => s.trim(),
(s) => s.replace(/\s+/g, '-'),
(s) => s.toLowerCase(),
);
console.log(normalize(' Hello World ')); // hello-world
三类作用域的差异见表 3-1。
| 作用域 | 创建位置 | 可见范围 | 典型问题 |
|---|---|---|---|
| 全局 | 脚本顶层 | 全脚本 | 命名冲突、被意外改写 |
| 模块 | type="module" 脚本 |
仅本文件 | 需要在出口显式 export |
| 函数 | 函数体 | 函数内部 | var 提升到函数顶 |
| 块级 | {} 内 |
块内 | var 不受块约束 |
闭包的两类风险见表 3-2。
| 风险 | 表现 | 处理 |
|---|---|---|
| 循环误捕获 | 所有回调读到同一个终值 | 循环变量用 let,或传参固化 |
| 内存滞留 | 大对象被长期持有,堆快照可见 | 用完后置 null,或缩短闭包生命周期 |
4 验证与讨论
- 执行脚本,确认输出
11、false、[warn] 磁盘空间不足、[0, 1, 2]、[3, 3, 3]、ReferenceError、106 110、hello-world。 - 把第三个演示的
let i改为var i,确认输出变为[3, 3, 3]。 - 在返回对象上执行
counter.count = 999,再读counter.value,确认私有变量不受影响。 - 用 Chrome 开发者工具的 Memory 面板拍两次堆快照,确认调用
counter后计数器变量仍被持有。
| 检查项 | 预期结果 | 说明 |
|---|---|---|
| 私有变量隔离 | 外部赋值不改变内部值 | 闭包内变量不在返回对象上 |
let 循环 |
每个回调拿到独立值 | 每轮新建绑定 |
var 循环 |
全部回调拿到终值 | 函数级作用域共享一个变量 |
| 暂时性死区 | 声明前访问抛 ReferenceError |
let/const 提升但不初始化 |
讨论与局限。 闭包不会复制变量,捕获的是同一个绑定,因此多个方法共享状态。另,模块顶层变量本就不进入全局对象,试图用 window.x 访问模块内变量会得到 undefined,这不是闭包特性而是模块特性。
5 常见问题
| 现象 | 原因 | 处理 |
|---|---|---|
| 循环回调输出全是最后一个值 | var 共享函数级作用域 |
改用 let 或用 forEach |
给 const 声明前赋值抛错 |
暂时性死区 | 把声明移到使用之前 |
| 事件回调读到旧值 | 闭包捕获了旧绑定 | 每次重建回调,或改用参数传入新值 |
| 内存持续增长 | 闭包持有大对象且长期存活 | 清理引用,避免在闭包中缓存大数组 |
| 模块内变量在控制台取不到 | 模块作用域不挂到 window |
显式挂载调试入口或加日志 |
6 结论
| 序号 | 结论 | 依据 |
|---|---|---|
| 1 | 查找沿词法作用域由内向外,单向不可逆 | 图 3-1 |
| 2 | 闭包捕获绑定而非值的拷贝 | 3.2 节 |
| 3 | 循环变量用 let 避免共享一个绑定 |
表 3-2 |
| 4 | 闭包延长变量生命周期,需主动清理引用 | 表 3-2 |
- 上一篇:B00-总结
- 下一篇:B02-this与原型链
- 返回:B00-总结 | 科目 README
参考文献
[1] MDN Web Docs · 闭包,https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Closures(访问于 2026-10-31)
[2] ECMA-262 · 9 Executable Code and Execution Contexts,https://tc39.es/ecma262/#sec-executable-code-and-execution-contexts(访问于 2026-10-31)

评论 0
还没有评论,快来抢沙发吧~