我将【闭包】讲给你听!
Category(分类): JavaScript Status: 已整理

图片来源:原文首图(原目录
image.png已转换为本地 WebP),已改为稳定相对路径。原图转载授权未核实,公开发布前请确认许可。
一、什么是闭包
MDN 对闭包的概括是:函数和对其周围状态(词法环境)的引用绑定在一起,这样的组合就是闭包;它让内层函数可以访问外层函数的作用域。ECMAScript 更精确的描述是,函数对象创建时会保存定义位置的词法环境引用,函数对象的 [[Environment]] 指向相应的 Environment Record。
从入门角度可以记住四点,但需要把原文的几处说法校正一下:
- 函数对象创建时会记录定义位置的词法环境;函数不必先执行、返回或“逃逸”才有环境引用。
- 闭包不是一个和函数并列的执行上下文。函数对象、闭包所指向的环境、调用函数时创建的执行上下文,是不同层次的概念。
- 环境记录保存标识符绑定,函数通过环境链查找自由变量;不是把作用域中的所有值复制进一个背包。
- 调用函数时会创建这一次调用的执行上下文;函数声明/表达式求值时已经创建函数对象。返回函数常常让闭合环境继续可达,所以更容易观察到闭包。
Environment Record 和执行上下文是规范抽象,不要求实现真的创建与图中一模一样的盒子。引擎可以优化未被观察到的变量,但必须保持可观察的绑定语义。
函数对象、调用和环境不是同一件事
下面的代码适合对应原文首图理解:
var baz
;(function () {
var foo = 1
baz = function () {
return foo
}
})()
console.log(baz())
// 1
逐步看:
- IIFE 的函数对象在函数表达式求值时已经创建。
- 调用 IIFE 时,才创建这一次 IIFE 调用的执行上下文。
- 内部函数对象创建时,把包含
foo的词法环境记录到[[Environment]]。 - IIFE 返回后,调用上下文结束,但
baz仍然引用内部函数,所以foo所在环境仍然可达。 baz()创建的是一次新的函数执行上下文,不是一个新的“函数实例”;只要baz仍可达,函数对象和闭合环境就不会因为某一行结束而立即销毁。
如果执行 baz = null,并且没有其他引用,相关对象和环境之后才可能变成可回收对象,具体 GC 时间由引擎决定。

图片来源:原文函数创建配图,已本地化。原图来自第三方文章资源,转载授权未核实;图片仅作教学示意。
二、函数引用外部变量后的闭包
function makeAdder() {
let sum = 0
return function (y) {
sum += y
return sum
}
}
const add1 = makeAdder()
const add2 = makeAdder()
console.log(add1(1))
console.log(add1(1))
console.log(add2(2))
console.log(add2(2))
输出为:
1
2
2
4
分析这段代码:
- 调用
makeAdder()一次,会创建一套新的sum绑定和一个新的返回函数对象。 - 返回函数的
[[Environment]]指向这次调用的环境;makeAdder的执行上下文结束后,该环境因返回函数仍然可达而继续存活。 add1的两次调用读写第一次makeAdder()的sum,所以得到1、2。add2来自第二次makeAdder()调用,它拥有另一套sum绑定,所以得到2、4。sum是活绑定,不是闭包中保存的一份不可变值。通过sum += y修改后,下一次调用会读取修改后的绑定。
变量查找也不是“先查本地,再特殊检查闭包”。函数执行上下文会从自己的 LexicalEnvironment 开始,沿 [[OuterEnv]] 链查找;这条链恰好包含 makeAdder 的环境和更外层环境。

图片来源:原文 makeAdder 配图,已本地化。原图来自第三方文章资源,转载授权未核实;图片仅作教学示意。
三、循环中闭包让人很意外
var:多个回调共享一个绑定
function starfunc() {
const tipText = [
'hello tom',
'hello jerry',
'hello jack',
]
for (var i = 0; i < tipText.length; i += 1) {
var item = tipText[i]
setTimeout(() => {
console.log(item)
}, 100)
}
}
starfunc()
常见输出是三次 hello jack。原因不是每个闭包保存了当时的值副本,而是:
var item的绑定属于starfunc的函数作用域。- 三个箭头函数分别创建,但都通过自己的环境链访问同一个
item绑定。 - 循环每轮给同一个绑定重新赋值;循环结束后绑定的值是
hello jack。 - timer task 稍后运行回调,回调读取的是当时仍然存在的共享绑定,因此三次都看到最后的值。
setTimeout 不会让回调在当前调用栈里“等待”,也不存在 ECMAScript 规范中的 Event Table:宿主注册 timer,达到延迟阈值后才有资格安排一个 task,回调开始运行时才进入 JavaScript 执行上下文。100ms 是调度下限,不是精确执行时刻。

图片来源:原文
var循环配图,已本地化。原图来自第三方文章资源,转载授权未核实;请确认图示的再发布许可。
let:每轮使用不同的迭代绑定
现代写法如下:
function starfuncWithLet() {
const tipText = [
'hello tom',
'hello jerry',
'hello jack',
]
for (let i = 0; i < tipText.length; i += 1) {
const item = tipText[i]
setTimeout(() => {
console.log(item)
}, 100)
}
}
starfuncWithLet()
// hello tom
// hello jerry
// hello jack
for (let ...) 的规范语义会为需要的迭代创建独立的每轮绑定,块内的 const item 也在每轮形成对应绑定。因此三个回调访问的是三个不同的 item,而不是三个值副本塞进同一个闭包。这里变化的是词法环境/绑定,不是“每轮创建一个可见的函数执行上下文”。
如果把 let i 提到循环外,再用普通的 for (; i < 3; i += 1),回调仍可能共享同一个外层 i;关键是每轮绑定的创建位置和语法语义,而不是关键字名字本身。

图片来源:原文
let循环配图,已本地化。原图来自第三方文章资源,转载授权未核实;图片仅作教学示意。
四、闭包与性能
原文提醒“构造函数内给每个实例创建方法”可能产生重复函数对象,这个观察在实例很多时有价值,但不能推出“闭包必然性能差”。是否有成本取决于捕获状态、对象数量、引擎优化和实际访问热点。
下面的构造函数会为每个实例创建两个自有函数:
function MyObject(name, message) {
this.name = String(name)
this.message = String(message)
this.getName = function () {
return this.name
}
this.getMessage = function () {
return this.message
}
}
如果方法只依赖 this,可以放到原型上,让多个实例共享同一个函数对象:
function MyObject(name, message) {
this.name = String(name)
this.message = String(message)
}
MyObject.prototype.getName = function () {
return this.name
}
MyObject.prototype.getMessage = function () {
return this.message
}
现代 class 的普通实例方法也位于原型上:
class ModernObject {
constructor(name, message) {
this.name = String(name)
this.message = String(message)
}
getName() {
return this.name
}
getMessage() {
return this.message
}
}
如果方法需要捕获每个实例的私有词法状态,闭包可能正是有意的封装方式;也可以考虑 #private 字段。原型方法不保证在任何引擎、任何调用场景下都更快,应该用对象数量、内存快照和基准测试判断。
五、小结
- 闭包是函数对象与定义位置词法环境的组合,不是独立的执行上下文。
var循环的回调共享一个函数作用域绑定;let循环可以产生每轮独立绑定。- timer 是宿主调度的 task,不会让回调在当前调用栈中等待。
- 外层函数返回后,执行上下文结束;若返回函数仍引用环境,环境中的绑定仍然可用。
- 闭包可以实现私有状态、函数工厂和部分应用;是否使用应由数据封装和生命周期决定。
- 原型方法常用于共享只依赖
this的行为,但不能用“闭包一定慢”或“原型一定快”这样的绝对结论替代测量。
图片来源与授权风险
五张配图均已从原文资源改为 images/102-image-01.webp 至 images/102-image-05.webp 的相对引用,正文不再依赖第三方热链。原作者、原站点和第三方图床的转载授权均未由仓库确认;公开发布前请确认图片许可,必要时替换为自绘图。
原文归属
作者:哇喔WEB。历史来源:SegmentFault 原文《我将【闭包】讲给你听!》。原文链接仅用于保留历史出处,当前语义以本文列出的规范和官方文档为准。
参考链接
- ECMAScript:Environment Records
- ECMAScript:Execution Contexts
- ECMAScript:ECMAScript Function Objects
- ECMAScript:for statement
- MDN:闭包
- MDN:for 语句
- MDN:setTimeout
- WHATWG HTML:timers
- MDN:继承与原型链
- MDN:Classes
- MDN:内存管理
原作者和来源信息沿用原文的文章归属;本地资源仅用于稳定展示,不代表对原图或原文的版权取得。